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 (
+
+
+
+
+
+
+
+
+ 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"
+ />
+
+
+
+
+
+
+
+
+ setEditForm(prev => ({ ...prev, avatar_url: e.target.value }))}
+ placeholder="https://..."
+ />
+
+
+
+
+
+
+
+ ) : (
+
+
+
+ {profile.display_name || profile.username}
+
+ {profile.display_name && (
+ @{profile.username}
+ )}
+
+
+ {profile.bio && (
+
+ {profile.bio}
+
+ )}
+
+
+
+
+ Joined {new Date(profile.created_at).toLocaleDateString('en-US', {
+ month: 'long',
+ year: 'numeric'
+ })}
+
+
+ {profile.location && (
+
+
+ {profile.location.city}, {profile.location.country}
+
+ )}
+
+
+ )}
+
+
+
+
+
+
+ {/* Stats Cards */}
+
+
+
+ {profile.ride_count}
+ Rides
+
+
+
+
+ {profile.coaster_count}
+ Coasters
+
+
+
+
+ {profile.park_count}
+ Parks
+
+
+
+
+ {profile.review_count}
+ Reviews
+
+
+
+
+ {/* Profile Tabs */}
+
+
+ Activity
+ Reviews
+ Top Lists
+ Ride Credits
+
+
+
+
+
+ Recent Activity
+
+ Latest reviews, ratings, and achievements
+
+
+
+
+
+
Activity Feed Coming Soon
+
+ Track reviews, ratings, and achievements
+
+
+
+
+
+
+
+
+
+ Reviews ({profile.review_count})
+
+ Parks and rides reviews
+
+
+
+
+
+
Reviews Coming Soon
+
+ User reviews and ratings will appear here
+
+
+
+
+
+
+
+
+
+ Top Lists
+
+ Personal rankings and favorite collections
+
+
+
+
+
+
Top Lists Coming Soon
+
+ Create and share your favorite park and ride rankings
+
+
+
+
+
+
+
+
+
+ Ride Credits
+
+ Track all the rides you've experienced
+
+
+
+
+
+
Ride Credits Coming Soon
+
+ Log and track your ride experiences
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/types/database.ts b/src/types/database.ts
index f33fe4d3..7d767c68 100644
--- a/src/types/database.ts
+++ b/src/types/database.ts
@@ -102,6 +102,8 @@ export interface Profile {
park_count: number;
review_count: number;
reputation_score: number;
+ created_at: string;
+ updated_at: string;
}
export interface Review {