https://altly.madebydanny.uk

Add landing page and history, increase upload limit

Implement a public landing page, a user history page to view past generations, and increase the daily upload limit to 20. Update the edge function to reflect the new rate limit and adjust routing to accommodate the landing page.

+373 -45
+6 -2
src/App.tsx
··· 3 3 import { TooltipProvider } from "@/components/ui/tooltip"; 4 4 import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 5 import { BrowserRouter, Routes, Route } from "react-router-dom"; 6 - import Index from "./pages/Index"; 6 + import Landing from "./pages/Landing"; 7 7 import Auth from "./pages/Auth"; 8 + import Index from "./pages/Index"; 9 + import History from "./pages/History"; 8 10 import NotFound from "./pages/NotFound"; 9 11 10 12 const queryClient = new QueryClient(); ··· 16 18 <Sonner /> 17 19 <BrowserRouter> 18 20 <Routes> 19 - <Route path="/" element={<Index />} /> 21 + <Route path="/" element={<Landing />} /> 20 22 <Route path="/auth" element={<Auth />} /> 23 + <Route path="/app" element={<Index />} /> 24 + <Route path="/history" element={<History />} /> 21 25 {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} 22 26 <Route path="*" element={<NotFound />} /> 23 27 </Routes>
+1 -1
src/components/ImageUploader.tsx
··· 122 122 123 123 let errorMessage = "Failed to generate alt text"; 124 124 if (error?.message?.includes('Rate limit')) { 125 - errorMessage = "Rate limit exceeded. Please try again in an hour."; 125 + errorMessage = "Daily limit reached (20 per day). Try again tomorrow."; 126 126 } else if (error?.message?.includes('Authentication')) { 127 127 errorMessage = "Please sign in to continue"; 128 128 }
+2 -2
src/pages/Auth.tsx
··· 20 20 // Check if user is already logged in 21 21 supabase.auth.getSession().then(({ data: { session } }) => { 22 22 if (session) { 23 - navigate("/"); 23 + navigate("/app"); 24 24 } 25 25 }); 26 26 27 27 const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { 28 28 if (session) { 29 - navigate("/"); 29 + navigate("/app"); 30 30 } 31 31 }); 32 32
+180
src/pages/History.tsx
··· 1 + import { useState, useEffect } from "react"; 2 + import { useNavigate } from "react-router-dom"; 3 + import { ArrowLeft, Copy, Check, Image as ImageIcon } from "lucide-react"; 4 + import { Button } from "@/components/ui/button"; 5 + import { Card } from "@/components/ui/card"; 6 + import { useToast } from "@/hooks/use-toast"; 7 + import { supabase } from "@/integrations/supabase/client"; 8 + import { User } from "@supabase/supabase-js"; 9 + 10 + interface Generation { 11 + id: string; 12 + image_url: string; 13 + alt_text: string; 14 + created_at: string; 15 + } 16 + 17 + export 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 + }
+48 -35
src/pages/Index.tsx
··· 1 1 import { useState, useEffect } from "react"; 2 2 import { useNavigate } from "react-router-dom"; 3 - import { Header } from "@/components/Header"; 4 3 import { Stats } from "@/components/Stats"; 5 4 import { ImageUploader } from "@/components/ImageUploader"; 6 - import { Eye, LogOut } from "lucide-react"; 5 + import { Eye, LogOut, History as HistoryIcon } from "lucide-react"; 7 6 import { Button } from "@/components/ui/button"; 8 7 import { supabase } from "@/integrations/supabase/client"; 9 8 import { User } from "@supabase/supabase-js"; 10 9 11 10 const Index = () => { 12 11 const [user, setUser] = useState<User | null>(null); 12 + const [dailyCount, setDailyCount] = useState(0); 13 13 const navigate = useNavigate(); 14 14 15 15 useEffect(() => { 16 - // Check current session 17 16 supabase.auth.getSession().then(({ data: { session } }) => { 18 17 if (session?.user) { 19 18 setUser(session.user); 19 + fetchDailyCount(session.user.id); 20 20 } else { 21 21 navigate("/auth"); 22 22 } 23 23 }); 24 24 25 - // Listen for auth changes 26 25 const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { 27 26 if (session?.user) { 28 27 setUser(session.user); 28 + fetchDailyCount(session.user.id); 29 29 } else { 30 30 navigate("/auth"); 31 31 } ··· 34 34 return () => subscription.unsubscribe(); 35 35 }, [navigate]); 36 36 37 + const fetchDailyCount = async (userId: string) => { 38 + const oneDayAgo = new Date(Date.now() - 86400000).toISOString(); 39 + const { data } = await supabase 40 + .from('alt_text_generations') 41 + .select('id') 42 + .eq('user_id', userId) 43 + .gte('created_at', oneDayAgo); 44 + 45 + setDailyCount(data?.length || 0); 46 + }; 47 + 37 48 const handleSignOut = async () => { 38 49 await supabase.auth.signOut(); 39 50 }; 40 51 41 - if (!user) { 42 - return null; // Will redirect to /auth 43 - } 52 + if (!user) return null; 44 53 45 54 return ( 46 55 <div className="min-h-screen bg-background"> 47 - <Header /> 48 - 49 - <main className="container mx-auto px-4 py-12"> 50 - {/* Hero Section */} 51 - <div className="text-center mb-12"> 52 - <div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary/20 mb-6"> 53 - <Eye className="w-8 h-8 text-primary" /> 56 + {/* Header */} 57 + <header className="border-b border-border/50 bg-card/50 backdrop-blur-sm sticky top-0 z-50"> 58 + <div className="container mx-auto px-4 py-4 flex items-center justify-between"> 59 + <div className="flex items-center gap-2"> 60 + <div className="w-10 h-10 rounded-xl bg-primary/20 flex items-center justify-center"> 61 + <Eye className="w-6 h-6 text-primary" /> 62 + </div> 63 + <span className="text-xl font-bold text-foreground">ALTly</span> 54 64 </div> 55 - <h1 className="text-4xl md:text-5xl font-bold text-foreground mb-4"> 56 - ALTly 57 - </h1> 58 - <p className="text-lg text-muted-foreground max-w-2xl mx-auto"> 59 - A free-for-life ALT text generator powered by Claude AI with a free public API. 60 - </p> 61 - <div className="mt-4 flex items-center justify-center gap-2 text-sm text-muted-foreground"> 62 - <span>Signed in as {user.email}</span> 65 + <div className="flex items-center gap-2"> 66 + <Button 67 + variant="ghost" 68 + onClick={() => navigate("/history")} 69 + className="gap-2" 70 + > 71 + <HistoryIcon className="w-4 h-4" /> 72 + <span className="hidden sm:inline">History</span> 73 + </Button> 63 74 <Button 64 75 variant="ghost" 65 76 size="sm" 66 77 onClick={handleSignOut} 67 - className="h-8" 68 78 > 69 79 <LogOut className="w-4 h-4 mr-1" /> 70 80 Sign Out 71 81 </Button> 72 82 </div> 73 83 </div> 84 + </header> 85 + 86 + <main className="container mx-auto px-4 py-12"> 87 + {/* Hero Section */} 88 + <div className="text-center mb-8"> 89 + <h1 className="text-3xl md:text-4xl font-bold text-foreground mb-2"> 90 + Generate Alt Text 91 + </h1> 92 + <p className="text-muted-foreground mb-2"> 93 + Upload an image to generate accessible alt text 94 + </p> 95 + <p className="text-sm text-muted-foreground"> 96 + Daily usage: <span className="font-semibold text-foreground">{dailyCount}/20</span> generations 97 + </p> 98 + </div> 74 99 75 100 {/* Stats Section */} 76 101 <Stats /> ··· 80 105 <ImageUploader user={user} /> 81 106 </div> 82 107 83 - {/* Footer */} 84 - <footer className="text-center text-sm text-muted-foreground py-8"> 85 - © 2024-2025 Made with ❤️ by{" "} 86 - <a 87 - href="https://github.com/dannyuk" 88 - target="_blank" 89 - rel="noopener noreferrer" 90 - className="text-primary hover:underline" 91 - > 92 - Danny UK 93 - </a> 94 - </footer> 95 108 </main> 96 109 </div> 97 110 );
+131
src/pages/Landing.tsx
··· 1 + import { useEffect } from "react"; 2 + import { useNavigate } from "react-router-dom"; 3 + import { Eye, Zap, Shield, Clock } from "lucide-react"; 4 + import { Button } from "@/components/ui/button"; 5 + import { Card } from "@/components/ui/card"; 6 + import { supabase } from "@/integrations/supabase/client"; 7 + 8 + export default function Landing() { 9 + const navigate = useNavigate(); 10 + 11 + useEffect(() => { 12 + // Check if user is already logged in 13 + supabase.auth.getSession().then(({ data: { session } }) => { 14 + if (session) { 15 + navigate("/app"); 16 + } 17 + }); 18 + }, [navigate]); 19 + 20 + return ( 21 + <div className="min-h-screen bg-background"> 22 + {/* Header */} 23 + <header className="border-b border-border/50 bg-card/50 backdrop-blur-sm sticky top-0 z-50"> 24 + <div className="container mx-auto px-4 py-4 flex items-center justify-between"> 25 + <div className="flex items-center gap-2"> 26 + <div className="w-10 h-10 rounded-xl bg-primary/20 flex items-center justify-center"> 27 + <Eye className="w-6 h-6 text-primary" /> 28 + </div> 29 + <span className="text-xl font-bold text-foreground">ALTly</span> 30 + </div> 31 + <div className="flex gap-2"> 32 + <Button variant="ghost" onClick={() => navigate("/auth")}> 33 + Sign In 34 + </Button> 35 + <Button onClick={() => navigate("/auth")}> 36 + Get Started 37 + </Button> 38 + </div> 39 + </div> 40 + </header> 41 + 42 + {/* Hero Section */} 43 + <section className="container mx-auto px-4 py-20 text-center"> 44 + <div className="inline-flex items-center justify-center w-20 h-20 rounded-3xl bg-primary/20 mb-8"> 45 + <Eye className="w-10 h-10 text-primary" /> 46 + </div> 47 + <h1 className="text-5xl md:text-7xl font-bold text-foreground mb-6 max-w-4xl mx-auto"> 48 + AI-Powered Alt Text 49 + <br /> 50 + <span className="text-primary">Made Simple</span> 51 + </h1> 52 + <p className="text-xl text-muted-foreground max-w-2xl mx-auto mb-8"> 53 + Generate perfect alt text for your images using Claude AI. Free, fast, and accessible for everyone. 54 + </p> 55 + <div className="flex gap-4 justify-center"> 56 + <Button size="lg" onClick={() => navigate("/auth")} className="text-lg px-8"> 57 + Start Generating 58 + </Button> 59 + <Button size="lg" variant="outline" onClick={() => navigate("/auth")} className="text-lg px-8"> 60 + Sign In 61 + </Button> 62 + </div> 63 + </section> 64 + 65 + {/* Features Section */} 66 + <section className="container mx-auto px-4 py-20"> 67 + <div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto"> 68 + <Card className="p-6 text-center"> 69 + <div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-primary/20 mb-4"> 70 + <Zap className="w-7 h-7 text-primary" /> 71 + </div> 72 + <h3 className="text-xl font-semibold text-foreground mb-2">Lightning Fast</h3> 73 + <p className="text-muted-foreground"> 74 + Generate alt text in seconds using Claude's powerful AI. No waiting, no hassle. 75 + </p> 76 + </Card> 77 + 78 + <Card className="p-6 text-center"> 79 + <div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-primary/20 mb-4"> 80 + <Shield className="w-7 h-7 text-primary" /> 81 + </div> 82 + <h3 className="text-xl font-semibold text-foreground mb-2">Secure & Private</h3> 83 + <p className="text-muted-foreground"> 84 + Your images are secure. Only you can see your generations and history. 85 + </p> 86 + </Card> 87 + 88 + <Card className="p-6 text-center"> 89 + <div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-primary/20 mb-4"> 90 + <Clock className="w-7 h-7 text-primary" /> 91 + </div> 92 + <h3 className="text-xl font-semibold text-foreground mb-2">20 Per Day</h3> 93 + <p className="text-muted-foreground"> 94 + Generate up to 20 alt texts daily. Perfect for content creators and developers. 95 + </p> 96 + </Card> 97 + </div> 98 + </section> 99 + 100 + {/* CTA Section */} 101 + <section className="container mx-auto px-4 py-20"> 102 + <Card className="max-w-3xl mx-auto p-12 text-center bg-gradient-to-br from-primary/10 to-primary/5"> 103 + <h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4"> 104 + Ready to Make Your Images Accessible? 105 + </h2> 106 + <p className="text-lg text-muted-foreground mb-8"> 107 + Join others who are making the web more accessible, one image at a time. 108 + </p> 109 + <Button size="lg" onClick={() => navigate("/auth")} className="text-lg px-8"> 110 + Get Started Free 111 + </Button> 112 + </Card> 113 + </section> 114 + 115 + {/* Footer */} 116 + <footer className="border-t border-border/50 py-8 mt-20"> 117 + <div className="container mx-auto px-4 text-center text-sm text-muted-foreground"> 118 + © 2024-2025 Made with ❤️ by{" "} 119 + <a 120 + href="https://github.com/dannyuk" 121 + target="_blank" 122 + rel="noopener noreferrer" 123 + className="text-primary hover:underline" 124 + > 125 + Danny UK 126 + </a> 127 + </div> 128 + </footer> 129 + </div> 130 + ); 131 + }
+5 -5
supabase/functions/generate-alt-text/index.ts
··· 73 73 ); 74 74 } 75 75 76 - // Check rate limit: max 10 requests per hour per user 77 - const oneHourAgo = new Date(Date.now() - 3600000).toISOString(); 76 + // Check rate limit: max 20 requests per day per user 77 + const oneDayAgo = new Date(Date.now() - 86400000).toISOString(); 78 78 const { data: recentGenerations, error: rateLimitError } = await supabase 79 79 .from('alt_text_generations') 80 80 .select('id') 81 81 .eq('user_id', user.id) 82 - .gte('created_at', oneHourAgo); 82 + .gte('created_at', oneDayAgo); 83 83 84 84 if (rateLimitError) { 85 85 console.error('Rate limit check error:', rateLimitError); ··· 89 89 ); 90 90 } 91 91 92 - if (recentGenerations && recentGenerations.length >= 10) { 92 + if (recentGenerations && recentGenerations.length >= 20) { 93 93 return new Response( 94 - JSON.stringify({ error: 'Rate limit exceeded. Maximum 10 requests per hour.' }), 94 + JSON.stringify({ error: 'Rate limit exceeded. Maximum 20 uploads per day.' }), 95 95 { status: 429, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } 96 96 ); 97 97 }