https://altly.madebydanny.uk
at main 180 lines 6.4 kB view raw
1import { useState, useEffect } from "react"; 2import { useNavigate } from "react-router-dom"; 3import { ArrowLeft, Copy, Check, Image as ImageIcon } from "lucide-react"; 4import { Button } from "@/components/ui/button"; 5import { Card } from "@/components/ui/card"; 6import { useToast } from "@/hooks/use-toast"; 7import { supabase } from "@/integrations/supabase/client"; 8import { User } from "@supabase/supabase-js"; 9 10interface Generation { 11 id: string; 12 image_url: string; 13 alt_text: string; 14 created_at: string; 15} 16 17export default function History() { 18 const [user, setUser] = useState<User | null>(null); 19 const [generations, setGenerations] = useState<Generation[]>([]); 20 const [isLoading, setIsLoading] = useState(true); 21 const [copiedId, setCopiedId] = useState<string | null>(null); 22 const navigate = useNavigate(); 23 const { toast } = useToast(); 24 25 useEffect(() => { 26 supabase.auth.getSession().then(({ data: { session } }) => { 27 if (session?.user) { 28 setUser(session.user); 29 fetchGenerations(session.user.id); 30 } else { 31 navigate("/auth"); 32 } 33 }); 34 35 const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { 36 if (session?.user) { 37 setUser(session.user); 38 fetchGenerations(session.user.id); 39 } else { 40 navigate("/auth"); 41 } 42 }); 43 44 return () => subscription.unsubscribe(); 45 }, [navigate]); 46 47 const fetchGenerations = async (userId: string) => { 48 setIsLoading(true); 49 try { 50 const { data, error } = await supabase 51 .from('alt_text_generations') 52 .select('*') 53 .eq('user_id', userId) 54 .order('created_at', { ascending: false }); 55 56 if (error) throw error; 57 setGenerations(data || []); 58 } catch (error) { 59 console.error('Error fetching generations:', error); 60 toast({ 61 title: "Error", 62 description: "Failed to load your history", 63 variant: "destructive", 64 }); 65 } finally { 66 setIsLoading(false); 67 } 68 }; 69 70 const copyToClipboard = async (text: string, id: string) => { 71 try { 72 await navigator.clipboard.writeText(text); 73 setCopiedId(id); 74 toast({ 75 title: "Copied!", 76 description: "Alt text copied to clipboard", 77 }); 78 setTimeout(() => setCopiedId(null), 2000); 79 } catch (error) { 80 toast({ 81 title: "Error", 82 description: "Failed to copy to clipboard", 83 variant: "destructive", 84 }); 85 } 86 }; 87 88 if (!user) return null; 89 90 return ( 91 <div className="min-h-screen bg-background"> 92 {/* Header */} 93 <header className="border-b border-border/50 bg-card/50 backdrop-blur-sm sticky top-0 z-50"> 94 <div className="container mx-auto px-4 py-4"> 95 <Button 96 variant="ghost" 97 onClick={() => navigate("/app")} 98 className="gap-2" 99 > 100 <ArrowLeft className="w-4 h-4" /> 101 Back to App 102 </Button> 103 </div> 104 </header> 105 106 <main className="container mx-auto px-4 py-12"> 107 <div className="max-w-4xl mx-auto"> 108 <div className="mb-8"> 109 <h1 className="text-3xl font-bold text-foreground mb-2">Your History</h1> 110 <p className="text-muted-foreground"> 111 View all your generated alt texts. You have {generations.length} generations. 112 </p> 113 </div> 114 115 {isLoading ? ( 116 <div className="text-center py-12"> 117 <p className="text-muted-foreground">Loading your history...</p> 118 </div> 119 ) : generations.length === 0 ? ( 120 <Card className="p-12 text-center"> 121 <ImageIcon className="w-12 h-12 text-muted-foreground mx-auto mb-4" /> 122 <h2 className="text-xl font-semibold text-foreground mb-2">No generations yet</h2> 123 <p className="text-muted-foreground mb-6"> 124 Upload your first image to get started 125 </p> 126 <Button onClick={() => navigate("/app")}> 127 Generate Alt Text 128 </Button> 129 </Card> 130 ) : ( 131 <div className="grid gap-6"> 132 {generations.map((generation) => ( 133 <Card key={generation.id} className="p-6"> 134 <div className="flex flex-col md:flex-row gap-6"> 135 <div className="w-full md:w-48 h-48 rounded-lg overflow-hidden bg-upload-area flex-shrink-0"> 136 <img 137 src={generation.image_url} 138 alt={generation.alt_text} 139 className="w-full h-full object-cover" 140 /> 141 </div> 142 <div className="flex-1 min-w-0"> 143 <div className="flex items-start justify-between gap-4 mb-3"> 144 <div className="flex-1"> 145 <p className="text-sm text-muted-foreground mb-2"> 146 {new Date(generation.created_at).toLocaleDateString('en-US', { 147 year: 'numeric', 148 month: 'long', 149 day: 'numeric', 150 hour: '2-digit', 151 minute: '2-digit' 152 })} 153 </p> 154 <h3 className="text-sm font-medium text-muted-foreground mb-2">Alt Text:</h3> 155 <p className="text-foreground leading-relaxed">{generation.alt_text}</p> 156 </div> 157 <Button 158 variant="ghost" 159 size="sm" 160 onClick={() => copyToClipboard(generation.alt_text, generation.id)} 161 className="flex-shrink-0" 162 > 163 {copiedId === generation.id ? ( 164 <Check className="w-4 h-4 text-primary" /> 165 ) : ( 166 <Copy className="w-4 h-4" /> 167 )} 168 </Button> 169 </div> 170 </div> 171 </div> 172 </Card> 173 ))} 174 </div> 175 )} 176 </div> 177 </main> 178 </div> 179 ); 180}