https://altly.madebydanny.uk
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}