elmeg-demo/frontend/app/bugs/page.tsx
fullsizemalt 14a509ddb5
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
feat: Add bug tracker MVP (decoupled, feature-flagged)
- Backend: Ticket and TicketComment models (no FK to User)
- API: /tickets/* endpoints for submit, view, comment, upvote
- Admin: /tickets/admin/* for triage queue
- Frontend: /bugs pages (submit, my-tickets, known-issues, detail)
- Feature flag: ENABLE_BUG_TRACKER env var (default: true)

To disable: Set ENABLE_BUG_TRACKER=false
To remove: Delete models_tickets.py, routers/tickets.py, frontend/app/bugs/
2025-12-23 13:18:00 -08:00

299 lines
14 KiB
TypeScript

"use client"
/**
* Bug Tracker - Main Page
* Submit bugs, feature requests, and questions
*/
import { useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Label } from "@/components/ui/label"
import {
Bug,
Lightbulb,
HelpCircle,
MessageSquare,
CheckCircle,
ArrowRight,
ExternalLink
} from "lucide-react"
import Link from "next/link"
import { getApiUrl } from "@/lib/api-config"
import { useAuth } from "@/contexts/auth-context"
const TICKET_TYPES = [
{ value: "bug", label: "Bug Report", icon: Bug, description: "Something isn't working" },
{ value: "feature", label: "Feature Request", icon: Lightbulb, description: "Suggest an improvement" },
{ value: "question", label: "Question", icon: HelpCircle, description: "Ask for help" },
{ value: "other", label: "Other", icon: MessageSquare, description: "General feedback" },
]
const PRIORITIES = [
{ value: "low", label: "Low", color: "bg-gray-500" },
{ value: "medium", label: "Medium", color: "bg-yellow-500" },
{ value: "high", label: "High", color: "bg-orange-500" },
{ value: "critical", label: "Critical", color: "bg-red-500" },
]
export default function BugsPage() {
const { user } = useAuth()
const [step, setStep] = useState<"type" | "form" | "success">("type")
const [selectedType, setSelectedType] = useState<string>("bug")
const [submitting, setSubmitting] = useState(false)
const [ticketNumber, setTicketNumber] = useState<string>("")
const [error, setError] = useState("")
const [formData, setFormData] = useState({
title: "",
description: "",
priority: "medium",
reporter_email: "",
reporter_name: "",
})
const handleTypeSelect = (type: string) => {
setSelectedType(type)
setStep("form")
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setSubmitting(true)
setError("")
try {
const token = localStorage.getItem("token")
const headers: Record<string, string> = {
"Content-Type": "application/json",
}
if (token) {
headers["Authorization"] = `Bearer ${token}`
}
const res = await fetch(`${getApiUrl()}/tickets/`, {
method: "POST",
headers,
body: JSON.stringify({
type: selectedType,
priority: formData.priority,
title: formData.title,
description: formData.description,
reporter_email: user?.email || formData.reporter_email,
reporter_name: formData.reporter_name,
page_url: window.location.href,
browser: navigator.userAgent,
}),
})
if (!res.ok) {
const data = await res.json()
throw new Error(data.detail || "Failed to submit")
}
const ticket = await res.json()
setTicketNumber(ticket.ticket_number)
setStep("success")
} catch (e: any) {
setError(e.message || "Failed to submit ticket")
} finally {
setSubmitting(false)
}
}
// Success screen
if (step === "success") {
return (
<div className="container max-w-2xl py-16">
<Card className="text-center">
<CardContent className="pt-12 pb-8 space-y-6">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 dark:bg-green-900">
<CheckCircle className="h-8 w-8 text-green-600 dark:text-green-400" />
</div>
<div>
<h1 className="text-2xl font-bold">Ticket Submitted!</h1>
<p className="text-muted-foreground mt-2">
Your ticket number is <span className="font-mono font-bold">{ticketNumber}</span>
</p>
</div>
<p className="text-sm text-muted-foreground">
We'll review your submission and get back to you soon.
</p>
<div className="flex gap-4 justify-center pt-4">
<Link href="/bugs/my-tickets">
<Button variant="outline">View My Tickets</Button>
</Link>
<Button onClick={() => { setStep("type"); setFormData({ title: "", description: "", priority: "medium", reporter_email: "", reporter_name: "" }) }}>
Submit Another
</Button>
</div>
</CardContent>
</Card>
</div>
)
}
return (
<div className="container max-w-4xl py-8">
{/* Header */}
<div className="text-center mb-12">
<h1 className="text-4xl font-bold tracking-tight mb-4">How can we help?</h1>
<p className="text-muted-foreground text-lg">
Report bugs, request features, or ask questions
</p>
</div>
{step === "type" && (
<>
{/* Type Selection */}
<div className="grid md:grid-cols-2 gap-4 mb-8">
{TICKET_TYPES.map((type) => {
const Icon = type.icon
return (
<button
key={type.value}
onClick={() => handleTypeSelect(type.value)}
className="text-left p-6 rounded-xl border-2 border-border hover:border-primary transition-colors bg-card hover:bg-accent group"
>
<div className="flex items-start gap-4">
<div className="p-3 rounded-lg bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
<Icon className="h-6 w-6" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-lg">{type.label}</h3>
<p className="text-muted-foreground text-sm">{type.description}</p>
</div>
<ArrowRight className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors" />
</div>
</button>
)
})}
</div>
{/* Quick Links */}
<div className="flex flex-wrap gap-4 justify-center">
<Link href="/bugs/known-issues">
<Button variant="outline" className="gap-2">
<ExternalLink className="h-4 w-4" />
Known Issues
</Button>
</Link>
{user && (
<Link href="/bugs/my-tickets">
<Button variant="outline" className="gap-2">
My Tickets
</Button>
</Link>
)}
</div>
</>
)}
{step === "form" && (
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={() => setStep("type")}>
← Back
</Button>
</div>
<CardTitle className="flex items-center gap-2">
{TICKET_TYPES.find(t => t.value === selectedType)?.icon && (
(() => {
const Icon = TICKET_TYPES.find(t => t.value === selectedType)!.icon
return <Icon className="h-5 w-5" />
})()
)}
{TICKET_TYPES.find(t => t.value === selectedType)?.label}
</CardTitle>
<CardDescription>
Fill out the details below
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Title */}
<div className="space-y-2">
<Label htmlFor="title">Title *</Label>
<Input
id="title"
placeholder="Brief summary of the issue"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
required
maxLength={200}
/>
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Provide more details. What happened? What did you expect?"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={5}
/>
</div>
{/* Priority */}
<div className="space-y-2">
<Label>Priority</Label>
<div className="flex gap-2">
{PRIORITIES.map((p) => (
<button
key={p.value}
type="button"
onClick={() => setFormData({ ...formData, priority: p.value })}
className={`px-4 py-2 rounded-lg border-2 transition-colors ${formData.priority === p.value
? "border-primary bg-primary/10"
: "border-border hover:border-muted-foreground"
}`}
>
<span className={`inline-block w-2 h-2 rounded-full ${p.color} mr-2`} />
{p.label}
</button>
))}
</div>
</div>
{/* Email (for anonymous) */}
{!user && (
<div className="space-y-2">
<Label htmlFor="email">Your Email *</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
value={formData.reporter_email}
onChange={(e) => setFormData({ ...formData, reporter_email: e.target.value })}
required
/>
<p className="text-xs text-muted-foreground">
We'll use this to send you updates
</p>
</div>
)}
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
<div className="flex gap-4 pt-4">
<Button type="button" variant="outline" onClick={() => setStep("type")}>
Cancel
</Button>
<Button type="submit" disabled={submitting || !formData.title}>
{submitting ? "Submitting..." : "Submit Ticket"}
</Button>
</div>
</form>
</CardContent>
</Card>
)}
</div>
)
}