diff --git a/app/api/support/tickets/[id]/route.ts b/app/api/support/tickets/[id]/route.ts
new file mode 100644
index 00000000..85270a71
--- /dev/null
+++ b/app/api/support/tickets/[id]/route.ts
@@ -0,0 +1,60 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { createClient } from '@/lib/supabase/server'
+
+export async function GET(
+ request: NextRequest,
+ { params }: { params: Promise<{ id:string }> }
+) {
+ const { id } = await params
+
+ try {
+ const supabase = await createClient()
+
+ const { data: { user }, error: authError } = await supabase.auth.getUser()
+
+ if (authError || !user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const { data: ticket, error } = await supabase
+ .from('support_tickets')
+ .select('*')
+ .eq('id', id)
+ .single()
+
+ if (error || !ticket) {
+ return NextResponse.json({ error: 'Ticket not found' }, { status: 404 })
+ }
+
+ if (ticket.user_id !== user.id) {
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
+ }
+
+ const { data: replies } = await supabase
+ .from('support_ticket_replies')
+ .select('id, admin_id, message, created_at')
+ .eq('ticket_id', id)
+ .order('created_at', { ascending: true })
+
+ const adminIds = [...new Set(replies?.map(r => r.admin_id) || [])]
+ const { data: adminProfiles } = await supabase
+ .from('profiles')
+ .select('id, first_name, last_name, avatar_url')
+ .in('id', adminIds)
+
+ const repliesWithAdmins = replies?.map(reply => ({
+ ...reply,
+ admin: adminProfiles?.find(p => p.id === reply.admin_id) || null
+ })) || []
+
+ const ticketWithReplies = {
+ ...ticket,
+ replies: repliesWithAdmins
+ }
+
+ return NextResponse.json({ ticket: ticketWithReplies })
+ } catch (error) {
+ console.error('Error in GET ticket:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
diff --git a/app/api/support/tickets/route.ts b/app/api/support/tickets/route.ts
new file mode 100644
index 00000000..3cad232b
--- /dev/null
+++ b/app/api/support/tickets/route.ts
@@ -0,0 +1,30 @@
+import { NextResponse } from 'next/server'
+import { createClient } from '@/lib/supabase/server'
+
+export async function GET() {
+ try {
+ const supabase = await createClient()
+
+ const { data: { user }, error: authError } = await supabase.auth.getUser()
+
+ if (authError || !user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const { data: tickets, error } = await supabase
+ .from('support_tickets')
+ .select('*')
+ .eq('user_id', user.id)
+ .order('created_at', { ascending: false })
+
+ if (error) {
+ console.error('Error fetching tickets:', error)
+ return NextResponse.json({ error: 'Failed to fetch tickets' }, { status: 500 })
+ }
+
+ return NextResponse.json({ tickets })
+ } catch (error) {
+ console.error('Error in GET tickets:', error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
diff --git a/app/protected/help/page.tsx b/app/protected/help/page.tsx
index 306fa8ea..3075e610 100644
--- a/app/protected/help/page.tsx
+++ b/app/protected/help/page.tsx
@@ -8,6 +8,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
import { Skeleton } from '@/components/ui/skeleton'
import { toast } from 'sonner'
+import TicketHistory from '@/components/TicketHistory'
import {
Search,
HelpCircle,
@@ -483,6 +484,9 @@ export default function HelpPage() {
)}
+ {/* Ticket History */}
+
+
{/* Quick Actions */}
{!searchQuery && (
diff --git a/app/protected/help/ticket/[id]/page.tsx b/app/protected/help/ticket/[id]/page.tsx
new file mode 100644
index 00000000..55494e11
--- /dev/null
+++ b/app/protected/help/ticket/[id]/page.tsx
@@ -0,0 +1,189 @@
+'use client'
+
+import React, { useState, useEffect } from 'react'
+import { useParams, useRouter } from 'next/navigation'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Badge } from '@/components/ui/badge'
+import { Skeleton } from '@/components/ui/skeleton'
+import { ArrowLeft, MessageSquare, Clock, Calendar, Bug, Mail } from 'lucide-react'
+import { toast } from 'sonner'
+import Link from 'next/link'
+
+interface TicketReply {
+ id: string
+ admin_id: string
+ message: string
+ created_at: string
+ admin?: {
+ first_name?: string
+ last_name?: string
+ avatar_url?: string
+ }
+}
+
+interface SupportTicket {
+ id: string
+ subject: string
+ message: string
+ status: 'open' | 'in_progress' | 'resolved' | 'closed'
+ created_at: string
+ updated_at: string
+ type: 'contact' | 'bug'
+ replies?: TicketReply[]
+}
+
+export default function UserTicketDetailPage() {
+ const params = useParams()
+ const router = useRouter()
+ const ticketId = params.id as string
+
+ const [ticket, setTicket] = useState
(null)
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ if (ticketId) {
+ fetchTicket()
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [ticketId])
+
+ const fetchTicket = async () => {
+ try {
+ const response = await fetch(`/api/support/tickets/${ticketId}`)
+ if (response.ok) {
+ const data = await response.json()
+ setTicket(data.ticket)
+ } else {
+ toast.error('Failed to load ticket details')
+ router.push('/protected/help')
+ }
+ } catch (error) {
+ console.error('Error fetching ticket:', error)
+ toast.error('Failed to load ticket details')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'open': return 'bg-red-500/10 text-red-400 border-red-500/20'
+ case 'in_progress': return 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'
+ case 'resolved': return 'bg-green-500/10 text-green-400 border-green-500/20'
+ case 'closed': return 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'
+ default: return 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'
+ }
+ }
+
+ if (loading) {
+ return (
+
+
+
+
+ )
+ }
+
+ if (!ticket) {
+ return (
+
+
+
+ Ticket not found
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
Ticket Details
+
ID: {ticket.id}
+
+
+ {ticket.status.replace('_', ' ')}
+
+
+
+
+
+
+ {ticket.type === 'bug' ? (
+
+ ) : (
+
+ )}
+ {ticket.subject}
+
+
+ {ticket.type === 'bug' ? 'Bug Report' : 'Support Request'}
+
+
+
+
+
+
+
+ Created: {new Date(ticket.created_at).toLocaleString()}
+
+
+
+ Last updated: {new Date(ticket.updated_at).toLocaleString()}
+
+
+
+
+
+ {ticket.replies && ticket.replies.length > 0 && (
+
+
+
+
+ Reply History
+
+
+
+ {ticket.replies.map((reply) => (
+
+
+
+ {reply.admin?.first_name?.[0] || 'A'}
+
+
+
+ {reply.admin?.first_name && reply.admin?.last_name
+ ? `${reply.admin.first_name} ${reply.admin.last_name}`
+ : 'Support Team'}
+
+
+ {new Date(reply.created_at).toLocaleString()}
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ )
+}
diff --git a/components/TicketHistory.tsx b/components/TicketHistory.tsx
new file mode 100644
index 00000000..4231f9ce
--- /dev/null
+++ b/components/TicketHistory.tsx
@@ -0,0 +1,105 @@
+'use client'
+
+import React, { useState, useEffect } from 'react'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+import { Skeleton } from '@/components/ui/skeleton'
+import { toast } from 'sonner'
+import { MessageSquare, Clock, ChevronRight } from 'lucide-react'
+import Link from 'next/link'
+
+interface SupportTicket {
+ id: string
+ subject: string
+ status: 'open' | 'in_progress' | 'resolved' | 'closed'
+ created_at: string
+ updated_at: string
+}
+
+export default function TicketHistory() {
+ const [tickets, setTickets] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ fetchTickets()
+ }, [])
+
+ const fetchTickets = async () => {
+ try {
+ const response = await fetch('/api/support/tickets')
+ if (response.ok) {
+ const data = await response.json()
+ setTickets(data.tickets)
+ } else {
+ toast.error('Failed to load ticket history')
+ }
+ } catch (error) {
+ console.error('Error fetching tickets:', error)
+ toast.error('Failed to load ticket history')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'open': return 'bg-red-500/10 text-red-400 border-red-500/20'
+ case 'in_progress': return 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'
+ case 'resolved': return 'bg-green-500/10 text-green-400 border-green-500/20'
+ case 'closed': return 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'
+ default: return 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'
+ }
+ }
+
+ if (loading) {
+ return (
+
+
+
+
+ My Support Tickets
+
+
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+
+ )
+ }
+
+ if (tickets.length === 0) {
+ return null
+ }
+
+ return (
+
+
+
+
+ My Support Tickets
+
+
+
+ {tickets.map((ticket) => (
+
+
+
+
{ticket.subject}
+
+
{ticket.status.replace('_', ' ')}
+
+
+ Last updated: {new Date(ticket.updated_at).toLocaleDateString()}
+
+
+
+
+
+
+ ))}
+
+
+ )
+}