298 lines
14 KiB
TypeScript
298 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 py-10 space-y-8 animate-in fade-in duration-700">
|
|
<div className="flex flex-col gap-2">
|
|
<h1 className="text-3xl font-bold tracking-tight">How can we help?</h1>
|
|
<p className="text-muted-foreground">
|
|
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-lg 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>
|
|
)
|
|
}
|