From 3a4b52ec18ee8e18c6ab24bc76dde96c0cd599fe Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 00:59:50 +0000 Subject: [PATCH] Add ride detail pages --- package-lock.json | 187 +++++++++++- package.json | 2 +- src/App.tsx | 40 ++- src/components/auth/AuthButtons.tsx | 109 +++++++ src/components/layout/Header.tsx | 8 +- src/hooks/useAuth.tsx | 104 +++++++ src/pages/Auth.tsx | 384 ++++++++++++++++++++++++ src/pages/Profile.tsx | 440 ++++++++++++++++++++++++++++ src/types/database.ts | 2 + 9 files changed, 1247 insertions(+), 29 deletions(-) create mode 100644 src/components/auth/AuthButtons.tsx create mode 100644 src/hooks/useAuth.tsx create mode 100644 src/pages/Auth.tsx create mode 100644 src/pages/Profile.tsx diff --git a/package-lock.json b/package-lock.json index 81c3e591..64df6ca1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-context-menu": "^2.2.15", "@radix-ui/react-dialog": "^1.1.14", - "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.14", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-menubar": "^1.1.15", @@ -1330,16 +1330,16 @@ } }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", - "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, @@ -1358,6 +1358,181 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", diff --git a/package.json b/package.json index 1b26160e..6a7437b7 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-context-menu": "^2.2.15", "@radix-ui/react-dialog": "^1.1.14", - "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.14", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-menubar": "^1.1.15", diff --git a/src/App.tsx b/src/App.tsx index 32afc193..41bf44be 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,34 +3,42 @@ import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { AuthProvider } from "@/hooks/useAuth"; import Index from "./pages/Index"; import Parks from "./pages/Parks"; import ParkDetail from "./pages/ParkDetail"; import RideDetail from "./pages/RideDetail"; import Rides from "./pages/Rides"; import Manufacturers from "./pages/Manufacturers"; +import Auth from "./pages/Auth"; +import Profile from "./pages/Profile"; import NotFound from "./pages/NotFound"; const queryClient = new QueryClient(); const App = () => ( - - - - - - } /> - } /> - } /> - } /> - } /> - } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} - } /> - - - + + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + + + ); diff --git a/src/components/auth/AuthButtons.tsx b/src/components/auth/AuthButtons.tsx new file mode 100644 index 00000000..b79bbe3f --- /dev/null +++ b/src/components/auth/AuthButtons.tsx @@ -0,0 +1,109 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Button } from '@/components/ui/button'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { User, Settings, LogOut, Trophy } from 'lucide-react'; +import { useAuth } from '@/hooks/useAuth'; +import { useToast } from '@/hooks/use-toast'; + +export function AuthButtons() { + const { user, profile, signOut } = useAuth(); + const navigate = useNavigate(); + const { toast } = useToast(); + const [loggingOut, setLoggingOut] = useState(false); + + const handleSignOut = async () => { + setLoggingOut(true); + try { + await signOut(); + toast({ + title: "Signed out", + description: "You've been signed out successfully.", + }); + navigate('/'); + } catch (error: any) { + toast({ + variant: "destructive", + title: "Error signing out", + description: error.message, + }); + } finally { + setLoggingOut(false); + } + }; + + if (!user) { + return ( + <> + + + + ); + } + + return ( + + + + + + +
+

+ {profile?.display_name || profile?.username} +

+

+ {user.email} +

+
+
+ + navigate('/profile')}> + + Profile + + navigate('/profile#lists')}> + + My Lists + + navigate('/profile#settings')}> + + Settings + + + + + {loggingOut ? 'Signing out...' : 'Sign out'} + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 1d0ce14f..ef92abcf 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -6,6 +6,7 @@ import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; import { Badge } from '@/components/ui/badge'; import { Link, useNavigate } from 'react-router-dom'; import { SearchDropdown } from '@/components/search/SearchDropdown'; +import { AuthButtons } from '@/components/auth/AuthButtons'; export function Header() { const navigate = useNavigate(); @@ -66,12 +67,7 @@ export function Header() { {/* User Actions */}
- - + {/* Mobile Menu */} diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx new file mode 100644 index 00000000..2b57b7f0 --- /dev/null +++ b/src/hooks/useAuth.tsx @@ -0,0 +1,104 @@ +import { createContext, useContext, useEffect, useState } from 'react'; +import { User, Session } from '@supabase/supabase-js'; +import { supabase } from '@/integrations/supabase/client'; +import { Profile } from '@/types/database'; + +interface AuthContextType { + user: User | null; + session: Session | null; + profile: Profile | null; + loading: boolean; + signOut: () => Promise; + refreshProfile: () => Promise; +} + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + const [session, setSession] = useState(null); + const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchProfile = async (userId: string) => { + try { + const { data, error } = await supabase + .from('profiles') + .select(`*, location:locations(*)`) + .eq('user_id', userId) + .maybeSingle(); + + if (error && error.code !== 'PGRST116') { + console.error('Error fetching profile:', error); + return; + } + + setProfile(data as Profile); + } catch (error) { + console.error('Error fetching profile:', error); + } + }; + + const refreshProfile = async () => { + if (user) { + await fetchProfile(user.id); + } + }; + + useEffect(() => { + // Get initial session + supabase.auth.getSession().then(({ data: { session } }) => { + setSession(session); + setUser(session?.user ?? null); + if (session?.user) { + fetchProfile(session.user.id); + } + setLoading(false); + }); + + // Listen for auth changes + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange(async (event, session) => { + setSession(session); + setUser(session?.user ?? null); + + if (session?.user) { + await fetchProfile(session.user.id); + } else { + setProfile(null); + } + + setLoading(false); + }); + + return () => subscription.unsubscribe(); + }, []); + + const signOut = async () => { + const { error } = await supabase.auth.signOut(); + if (error) { + console.error('Error signing out:', error); + throw error; + } + }; + + const value = { + user, + session, + profile, + loading, + signOut, + refreshProfile, + }; + + return {children}; +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/pages/Auth.tsx b/src/pages/Auth.tsx new file mode 100644 index 00000000..03bb04d3 --- /dev/null +++ b/src/pages/Auth.tsx @@ -0,0 +1,384 @@ +import { useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { Header } from '@/components/layout/Header'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Separator } from '@/components/ui/separator'; +import { Zap, Mail, Lock, User, AlertCircle, Eye, EyeOff } from 'lucide-react'; +import { supabase } from '@/integrations/supabase/client'; +import { useToast } from '@/hooks/use-toast'; + +export default function Auth() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const { toast } = useToast(); + const [loading, setLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [formData, setFormData] = useState({ + email: '', + password: '', + confirmPassword: '', + username: '', + displayName: '' + }); + + const defaultTab = searchParams.get('tab') || 'signin'; + + const handleInputChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ + ...prev, + [e.target.name]: e.target.value + })); + }; + + const handleSignIn = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + try { + const { data, error } = await supabase.auth.signInWithPassword({ + email: formData.email, + password: formData.password, + }); + + if (error) throw error; + + toast({ + title: "Welcome back!", + description: "You've been signed in successfully.", + }); + + const redirectTo = searchParams.get('redirect') || '/'; + navigate(redirectTo); + } catch (error: any) { + toast({ + variant: "destructive", + title: "Sign in failed", + description: error.message, + }); + } finally { + setLoading(false); + } + }; + + const handleSignUp = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + if (formData.password !== formData.confirmPassword) { + toast({ + variant: "destructive", + title: "Passwords don't match", + description: "Please make sure your passwords match.", + }); + setLoading(false); + return; + } + + if (formData.password.length < 6) { + toast({ + variant: "destructive", + title: "Password too short", + description: "Password must be at least 6 characters long.", + }); + setLoading(false); + return; + } + + try { + const { data, error } = await supabase.auth.signUp({ + email: formData.email, + password: formData.password, + options: { + data: { + username: formData.username, + display_name: formData.displayName + } + } + }); + + if (error) throw error; + + toast({ + title: "Welcome to ThrillWiki!", + description: "Please check your email to verify your account.", + }); + + navigate('/'); + } catch (error: any) { + toast({ + variant: "destructive", + title: "Sign up failed", + description: error.message, + }); + } finally { + setLoading(false); + } + }; + + const handleSocialSignIn = async (provider: 'google' | 'github') => { + try { + const { error } = await supabase.auth.signInWithOAuth({ + provider, + options: { + redirectTo: `${window.location.origin}/auth/callback` + } + }); + + if (error) throw error; + } catch (error: any) { + toast({ + variant: "destructive", + title: "Social sign in failed", + description: error.message, + }); + } + }; + + return ( +
+
+ +
+
+
+
+
+ +
+
+

+ ThrillWiki +

+
+

+ Join the ultimate theme park community +

+
+ + + + + Sign In + Sign Up + + + + + Welcome back + + Sign in to your ThrillWiki account + + + +
+
+ +
+ + +
+
+ +
+ +
+ + + +
+
+ + +
+ +
+
+
+ +
+
+ + Or continue with + +
+
+ +
+ + +
+
+
+
+ + + + Create account + + Join the ThrillWiki community today + + + +
+
+
+ +
+ + +
+
+
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + + +
+
+ +
+ +
+ + +
+
+ + +
+ + + + + By signing up, you agree to our Terms of Service and Privacy Policy. + + +
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx new file mode 100644 index 00000000..f3bfe04c --- /dev/null +++ b/src/pages/Profile.tsx @@ -0,0 +1,440 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { Header } from '@/components/layout/Header'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { + User, + MapPin, + Calendar, + Star, + Trophy, + Settings, + Camera, + Edit3, + Save, + X, + ArrowLeft +} from 'lucide-react'; +import { Profile as ProfileType } from '@/types/database'; +import { supabase } from '@/integrations/supabase/client'; +import { useToast } from '@/hooks/use-toast'; + +export default function Profile() { + const { username } = useParams<{ username?: string }>(); + const navigate = useNavigate(); + const { toast } = useToast(); + const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(true); + const [editing, setEditing] = useState(false); + const [currentUser, setCurrentUser] = useState(null); + const [editForm, setEditForm] = useState({ + display_name: '', + bio: '', + avatar_url: '' + }); + + useEffect(() => { + getCurrentUser(); + if (username) { + fetchProfile(username); + } else { + fetchCurrentUserProfile(); + } + }, [username]); + + const getCurrentUser = async () => { + const { data: { user } } = await supabase.auth.getUser(); + setCurrentUser(user); + }; + + const fetchProfile = async (profileUsername: string) => { + try { + const { data, error } = await supabase + .from('profiles') + .select(`*, location:locations(*)`) + .eq('username', profileUsername) + .maybeSingle(); + + if (error) throw error; + + if (data) { + setProfile(data as ProfileType); + setEditForm({ + display_name: data.display_name || '', + bio: data.bio || '', + avatar_url: data.avatar_url || '' + }); + } + } catch (error: any) { + console.error('Error fetching profile:', error); + toast({ + variant: "destructive", + title: "Error loading profile", + description: error.message, + }); + } finally { + setLoading(false); + } + }; + + const fetchCurrentUserProfile = async () => { + try { + const { data: { user } } = await supabase.auth.getUser(); + + if (!user) { + navigate('/auth'); + return; + } + + const { data, error } = await supabase + .from('profiles') + .select(`*, location:locations(*)`) + .eq('user_id', user.id) + .maybeSingle(); + + if (error) throw error; + + if (data) { + setProfile(data as ProfileType); + setEditForm({ + display_name: data.display_name || '', + bio: data.bio || '', + avatar_url: data.avatar_url || '' + }); + } + } catch (error: any) { + console.error('Error fetching profile:', error); + toast({ + variant: "destructive", + title: "Error loading profile", + description: error.message, + }); + } finally { + setLoading(false); + } + }; + + const handleSaveProfile = async () => { + if (!profile || !currentUser) return; + + try { + const { error } = await supabase + .from('profiles') + .update({ + display_name: editForm.display_name, + bio: editForm.bio, + avatar_url: editForm.avatar_url + }) + .eq('user_id', currentUser.id); + + if (error) throw error; + + setProfile(prev => prev ? { + ...prev, + display_name: editForm.display_name, + bio: editForm.bio, + avatar_url: editForm.avatar_url + } : null); + + setEditing(false); + toast({ + title: "Profile updated", + description: "Your profile has been updated successfully.", + }); + } catch (error: any) { + toast({ + variant: "destructive", + title: "Error updating profile", + description: error.message, + }); + } + }; + + const isOwnProfile = currentUser && profile && currentUser.id === profile.user_id; + + if (loading) { + return ( +
+
+
+
+
+
+
+
+
+
+ ); + } + + if (!profile) { + return ( +
+
+
+
+ +

Profile Not Found

+

+ The profile you're looking for doesn't exist. +

+ +
+
+
+ ); + } + + return ( +
+
+ +
+ {/* Profile Header */} +
+ + +
+
+ + + + {(profile.display_name || profile.username).charAt(0).toUpperCase()} + + + + {isOwnProfile && !editing && ( + + )} +
+ +
+ {editing && isOwnProfile ? ( +
+
+ + setEditForm(prev => ({ ...prev, display_name: e.target.value }))} + placeholder="Your display name" + /> +
+ +
+ +