POL-125: Frontend visual upgrade — dark luxury pole dance theme

- New dark theme with rose/purple/gold accent palette
- Premium typography: Cormorant Garamond (display) + Outfit (body)
- Glassmorphism cards, gradient mesh backgrounds, glow effects
- CSS split into theme.css, utilities.css, animations.css
- Staggered fade-in animations on list pages
- Redesigned all pages: auth, championships, registrations, profile, admin
- Lucide icons replace emoji throughout
- Responsive mobile nav with hamburger menu

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dianaka123
2026-02-27 13:56:45 +03:00
parent cf4104069e
commit 6fbd0326fa
24 changed files with 882 additions and 342 deletions

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useEffect } from "react";
import { useUsers, useUserActions } from "@/hooks/useUsers"; import { useUsers, useUserActions } from "@/hooks/useUsers";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { UserCard } from "@/components/admin/UserCard"; import { UserCard } from "@/components/admin/UserCard";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { Shield } from "lucide-react";
type Filter = "pending" | "all"; type Filter = "pending" | "all";
@@ -20,47 +20,63 @@ export default function AdminPage() {
if (user && user.role !== "admin") router.replace("/championships"); if (user && user.role !== "admin") router.replace("/championships");
}, [user, router]); }, [user, router]);
if (isLoading) return <div className="flex justify-center py-20 text-gray-400">Loading</div>; if (isLoading) {
if (error) return <p className="text-center text-red-500 py-20">Failed to load users.</p>; return (
<div className="flex justify-center py-20">
<div className="relative h-8 w-8">
<div className="absolute inset-0 rounded-full border-2 border-rose-accent/20" />
<div className="absolute inset-0 rounded-full border-2 border-transparent border-t-rose-accent animate-spin" />
</div>
</div>
);
}
if (error) return <p className="text-center text-destructive py-20">Failed to load users.</p>;
const pending = data?.filter((u) => u.status === "pending") ?? []; const pending = data?.filter((u) => u.status === "pending") ?? [];
const shown = filter === "pending" ? pending : (data ?? []); const shown = filter === "pending" ? pending : (data ?? []);
return ( return (
<div> <div className="animate-fade-in">
<h1 className="mb-6 text-2xl font-bold text-gray-900">User Management</h1> <div className="mb-8">
<h1 className="font-display text-4xl font-bold tracking-wide">User Management</h1>
<p className="mt-1 text-muted-foreground">Review and manage user accounts</p>
</div>
<div className="mb-4 flex gap-2"> <div className="mb-5 flex gap-2">
<button {(["pending", "all"] as const).map((f) => (
onClick={() => setFilter("pending")} <button
className={`rounded-full px-4 py-1.5 text-sm font-medium transition-colors ${ key={f}
filter === "pending" ? "bg-violet-600 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200" onClick={() => setFilter(f)}
}`} className={`rounded-full px-4 py-1.5 text-sm font-medium transition-all duration-200 ${
> filter === f
Pending ({pending.length}) ? "bg-rose-accent text-white glow-rose"
</button> : "bg-surface-elevated border border-border/40 text-muted-foreground hover:text-foreground hover:border-border"
<button }`}
onClick={() => setFilter("all")} >
className={`rounded-full px-4 py-1.5 text-sm font-medium transition-colors ${ {f === "pending" ? `Pending (${pending.length})` : `All users (${data?.length ?? 0})`}
filter === "all" ? "bg-violet-600 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200" </button>
}`} ))}
>
All users ({data?.length ?? 0})
</button>
</div> </div>
{shown.length === 0 ? ( {shown.length === 0 ? (
<p className="text-center text-gray-400 py-12">No users in this category.</p> <div className="text-center py-12">
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-surface-elevated border border-border/40">
<Shield className="h-6 w-6 text-dim" />
</div>
<p className="text-muted-foreground">No users in this category.</p>
</div>
) : ( ) : (
<div className="space-y-3 max-w-2xl"> <div className="space-y-3 max-w-2xl">
{shown.map((u) => ( {shown.map((u, i) => (
<UserCard <div key={u.id} className={`animate-fade-in-up stagger-${Math.min(i + 1, 9)}`}>
key={u.id} <UserCard
user={u} user={u}
onApprove={(id) => approve.mutate(id)} onApprove={(id) => approve.mutate(id)}
onReject={(id) => reject.mutate(id)} onReject={(id) => reject.mutate(id)}
isActing={approve.isPending || reject.isPending} isActing={approve.isPending || reject.isPending}
/> />
</div>
))} ))}
</div> </div>
)} )}

View File

@@ -4,22 +4,30 @@ import { use } from "react";
import { useChampionship } from "@/hooks/useChampionships"; import { useChampionship } from "@/hooks/useChampionships";
import { useMyRegistrations } from "@/hooks/useRegistrations"; import { useMyRegistrations } from "@/hooks/useRegistrations";
import { useRegisterForChampionship } from "@/hooks/useRegistrations"; import { useRegisterForChampionship } from "@/hooks/useRegistrations";
import { useAuth } from "@/hooks/useAuth";
import { RegistrationTimeline } from "@/components/registrations/RegistrationTimeline"; import { RegistrationTimeline } from "@/components/registrations/RegistrationTimeline";
import { StatusBadge } from "@/components/shared/StatusBadge"; import { StatusBadge } from "@/components/shared/StatusBadge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { MapPin, Building2, Calendar, CreditCard, Film, ExternalLink } from "lucide-react";
export default function ChampionshipDetailPage({ params }: { params: Promise<{ id: string }> }) { export default function ChampionshipDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params); const { id } = use(params);
const user = useAuth((s) => s.user);
const { data: championship, isLoading, error } = useChampionship(id); const { data: championship, isLoading, error } = useChampionship(id);
const { data: myRegs } = useMyRegistrations(); const { data: myRegs } = useMyRegistrations();
const registerMutation = useRegisterForChampionship(id); const registerMutation = useRegisterForChampionship(id);
if (isLoading) return <div className="flex justify-center py-20 text-gray-400">Loading</div>; if (isLoading) {
if (error || !championship) return <p className="text-center text-red-500 py-20">Championship not found.</p>; return (
<div className="flex justify-center py-20">
<div className="relative h-8 w-8">
<div className="absolute inset-0 rounded-full border-2 border-rose-accent/20" />
<div className="absolute inset-0 rounded-full border-2 border-transparent border-t-rose-accent animate-spin" />
</div>
</div>
);
}
if (error || !championship) return <p className="text-center text-destructive py-20">Championship not found.</p>;
const myReg = myRegs?.find((r) => r.championship_id === id); const myReg = myRegs?.find((r) => r.championship_id === id);
const canRegister = championship.status === "open" && !myReg; const canRegister = championship.status === "open" && !myReg;
@@ -29,51 +37,77 @@ export default function ChampionshipDetailPage({ params }: { params: Promise<{ i
: null; : null;
return ( return (
<div className="max-w-2xl mx-auto space-y-6"> <div className="max-w-2xl mx-auto space-y-6 animate-fade-in">
{/* Header image */} {/* Header image */}
{championship.image_url ? ( <div className="relative overflow-hidden rounded-2xl">
<img src={championship.image_url} alt={championship.title} className="w-full h-56 object-cover rounded-xl" /> {championship.image_url ? (
) : ( <img src={championship.image_url} alt={championship.title} className="w-full h-56 object-cover" />
<div className="w-full h-56 rounded-xl bg-gradient-to-br from-violet-400 to-purple-600 flex items-center justify-center text-6xl"> ) : (
🏆 <div className="w-full h-56 bg-gradient-to-br from-rose-accent/20 via-purple-accent/15 to-gold-accent/10 flex items-center justify-center">
</div> <span className="text-6xl opacity-30">💃</span>
)} </div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/20 to-transparent" />
</div>
{/* Title + status */} {/* Title + status */}
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div> <div>
<h1 className="text-2xl font-bold text-gray-900">{championship.title}</h1> <h1 className="font-display text-3xl font-bold tracking-wide">{championship.title}</h1>
{championship.subtitle && <p className="text-gray-500 mt-1">{championship.subtitle}</p>} {championship.subtitle && <p className="text-muted-foreground mt-1">{championship.subtitle}</p>}
</div> </div>
<StatusBadge status={championship.status} /> <StatusBadge status={championship.status} />
</div> </div>
{/* Details */} {/* Details grid */}
<div className="space-y-2 text-sm text-gray-600"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{championship.location && <p>📍 {championship.location}</p>} {championship.location && (
{championship.venue && <p>🏛 {championship.venue}</p>} <div className="flex items-center gap-2.5 rounded-xl bg-surface-elevated border border-border/30 px-4 py-3 text-sm">
{eventDate && <p>📅 {eventDate}</p>} <MapPin size={16} className="text-rose-accent shrink-0" />
{championship.entry_fee != null && <p>💳 Entry fee: <strong>{championship.entry_fee} </strong></p>} <span className="text-muted-foreground">{championship.location}</span>
</div>
)}
{championship.venue && (
<div className="flex items-center gap-2.5 rounded-xl bg-surface-elevated border border-border/30 px-4 py-3 text-sm">
<Building2 size={16} className="text-purple-accent shrink-0" />
<span className="text-muted-foreground">{championship.venue}</span>
</div>
)}
{eventDate && (
<div className="flex items-center gap-2.5 rounded-xl bg-surface-elevated border border-border/30 px-4 py-3 text-sm">
<Calendar size={16} className="text-gold-accent shrink-0" />
<span className="text-muted-foreground">{eventDate}</span>
</div>
)}
{championship.entry_fee != null && (
<div className="flex items-center gap-2.5 rounded-xl bg-surface-elevated border border-border/30 px-4 py-3 text-sm">
<CreditCard size={16} className="text-gold-accent shrink-0" />
<span className="text-muted-foreground">Entry fee: <strong className="text-foreground">{championship.entry_fee} </strong></span>
</div>
)}
{championship.video_max_duration != null && ( {championship.video_max_duration != null && (
<p>🎬 Max video: <strong>{Math.floor(championship.video_max_duration / 60)}m {championship.video_max_duration % 60}s</strong></p> <div className="flex items-center gap-2.5 rounded-xl bg-surface-elevated border border-border/30 px-4 py-3 text-sm">
<Film size={16} className="text-purple-accent shrink-0" />
<span className="text-muted-foreground">Max video: <strong className="text-foreground">{Math.floor(championship.video_max_duration / 60)}m {championship.video_max_duration % 60}s</strong></span>
</div>
)} )}
</div> </div>
{championship.description && ( {championship.description && (
<> <>
<Separator /> <Separator className="bg-border/30" />
<p className="text-gray-700 whitespace-pre-line">{championship.description}</p> <p className="text-muted-foreground whitespace-pre-line leading-relaxed">{championship.description}</p>
</> </>
)} )}
<Separator /> <Separator className="bg-border/30" />
{/* Registration section */} {/* Registration section */}
{myReg && <RegistrationTimeline registration={myReg} />} {myReg && <RegistrationTimeline registration={myReg} />}
{canRegister && ( {canRegister && (
<Button <Button
className="w-full bg-violet-600 hover:bg-violet-700" className="w-full bg-rose-accent hover:bg-rose-accent/90 text-white font-medium tracking-wide h-11"
disabled={registerMutation.isPending} disabled={registerMutation.isPending}
onClick={() => registerMutation.mutate()} onClick={() => registerMutation.mutate()}
> >
@@ -82,7 +116,7 @@ export default function ChampionshipDetailPage({ params }: { params: Promise<{ i
)} )}
{championship.status !== "open" && !myReg && ( {championship.status !== "open" && !myReg && (
<p className="text-center text-sm text-gray-400">Registration is not open.</p> <p className="text-center text-sm text-dim">Registration is not open.</p>
)} )}
{championship.form_url && ( {championship.form_url && (
@@ -90,9 +124,10 @@ export default function ChampionshipDetailPage({ params }: { params: Promise<{ i
href={championship.form_url} href={championship.form_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="block text-center text-sm text-violet-600 hover:underline" className="flex items-center justify-center gap-1.5 text-sm text-rose-accent hover:text-rose-accent/80 transition-colors"
> >
Open registration form Open registration form
<ExternalLink size={14} />
</a> </a>
)} )}
</div> </div>

View File

@@ -2,20 +2,46 @@
import { useChampionships } from "@/hooks/useChampionships"; import { useChampionships } from "@/hooks/useChampionships";
import { ChampionshipCard } from "@/components/championships/ChampionshipCard"; import { ChampionshipCard } from "@/components/championships/ChampionshipCard";
import { Trophy } from "lucide-react";
export default function ChampionshipsPage() { export default function ChampionshipsPage() {
const { data, isLoading, error } = useChampionships(); const { data, isLoading, error } = useChampionships();
if (isLoading) return <div className="flex justify-center py-20 text-gray-400">Loading</div>; if (isLoading) {
if (error) return <p className="text-center text-red-500 py-20">Failed to load championships.</p>; return (
if (!data?.length) return <p className="text-center text-gray-400 py-20">No championships yet.</p>; <div className="flex justify-center py-20">
<div className="relative h-8 w-8">
<div className="absolute inset-0 rounded-full border-2 border-rose-accent/20" />
<div className="absolute inset-0 rounded-full border-2 border-transparent border-t-rose-accent animate-spin" />
</div>
</div>
);
}
if (error) return <p className="text-center text-destructive py-20">Failed to load championships.</p>;
if (!data?.length) {
return (
<div className="text-center py-20 animate-fade-in">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-surface-elevated border border-border/40">
<Trophy className="h-7 w-7 text-dim" />
</div>
<p className="text-muted-foreground">No championships yet.</p>
</div>
);
}
return ( return (
<div> <div className="animate-fade-in">
<h1 className="mb-6 text-2xl font-bold text-gray-900">Championships</h1> <div className="mb-8">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <h1 className="font-display text-4xl font-bold tracking-wide">Championships</h1>
{data.map((c) => ( <p className="mt-1 text-muted-foreground">Browse upcoming competitions</p>
<ChampionshipCard key={c.id} championship={c} /> </div>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{data.map((c, i) => (
<div key={c.id} className={`animate-fade-in-up stagger-${Math.min(i + 1, 9)}`}>
<ChampionshipCard championship={c} />
</div>
))} ))}
</div> </div>
</div> </div>

View File

@@ -2,9 +2,9 @@ import { Navbar } from "@/components/layout/Navbar";
export default function AppLayout({ children }: { children: React.ReactNode }) { export default function AppLayout({ children }: { children: React.ReactNode }) {
return ( return (
<> <div className="min-h-screen bg-mesh">
<Navbar /> <Navbar />
<main className="mx-auto max-w-6xl px-4 py-8">{children}</main> <main className="mx-auto max-w-6xl px-4 py-8">{children}</main>
</> </div>
); );
} }

View File

@@ -6,11 +6,12 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Phone, Building2, AtSign, CalendarDays, LogOut } from "lucide-react";
const ROLE_COLORS: Record<string, string> = { const ROLE_COLORS: Record<string, string> = {
admin: "bg-red-100 text-red-700", admin: "bg-destructive/15 text-destructive border-destructive/20",
organizer: "bg-violet-100 text-violet-700", organizer: "bg-purple-soft text-purple-accent border-purple-accent/20",
member: "bg-green-100 text-green-700", member: "bg-rose-soft text-rose-accent border-rose-accent/20",
}; };
export default function ProfilePage() { export default function ProfilePage() {
@@ -29,52 +30,73 @@ export default function ProfilePage() {
} }
return ( return (
<div className="max-w-md mx-auto space-y-6"> <div className="max-w-md mx-auto space-y-6 animate-fade-in">
<div className="flex flex-col items-center gap-3 pt-4"> <div className="flex flex-col items-center gap-4 pt-4">
<Avatar className="h-20 w-20"> {/* Avatar with gradient ring */}
<AvatarFallback className="bg-violet-100 text-violet-700 text-2xl font-bold"> <div className="relative">
{initials} <div className="absolute -inset-1 rounded-full bg-gradient-to-br from-rose-accent via-purple-accent to-gold-accent opacity-50 blur-sm" />
</AvatarFallback> <Avatar className="relative h-20 w-20 border-2 border-background">
</Avatar> <AvatarFallback className="bg-surface-elevated text-rose-accent text-2xl font-bold">
<div className="text-center"> {initials}
<p className="text-xl font-bold text-gray-900">{user.full_name}</p> </AvatarFallback>
<p className="text-sm text-gray-500">{user.email}</p> </Avatar>
</div> </div>
<Badge className={`${ROLE_COLORS[user.role] ?? "bg-gray-100"} border-0 capitalize`}>
<div className="text-center">
<p className="font-display text-2xl font-bold tracking-wide">{user.full_name}</p>
<p className="text-sm text-muted-foreground mt-1">{user.email}</p>
</div>
<Badge className={`${ROLE_COLORS[user.role] ?? "bg-surface-elevated text-muted-foreground"} border capitalize`}>
{user.role} {user.role}
</Badge> </Badge>
</div> </div>
<Separator /> <Separator className="bg-border/30" />
<div className="space-y-3 text-sm text-gray-700"> <div className="space-y-1 rounded-xl bg-surface-elevated border border-border/30 p-4">
{user.phone && ( {user.phone && (
<div className="flex justify-between"> <div className="flex items-center justify-between py-2">
<span className="text-gray-400">Phone</span> <span className="flex items-center gap-2 text-sm text-dim">
<span>{user.phone}</span> <Phone size={14} />
Phone
</span>
<span className="text-sm text-foreground">{user.phone}</span>
</div> </div>
)} )}
{user.organization_name && ( {user.organization_name && (
<div className="flex justify-between"> <div className="flex items-center justify-between py-2">
<span className="text-gray-400">Organization</span> <span className="flex items-center gap-2 text-sm text-dim">
<span>{user.organization_name}</span> <Building2 size={14} />
Organization
</span>
<span className="text-sm text-foreground">{user.organization_name}</span>
</div> </div>
)} )}
{user.instagram_handle && ( {user.instagram_handle && (
<div className="flex justify-between"> <div className="flex items-center justify-between py-2">
<span className="text-gray-400">Instagram</span> <span className="flex items-center gap-2 text-sm text-dim">
<span>{user.instagram_handle}</span> <AtSign size={14} />
AtSign
</span>
<span className="text-sm text-foreground">{user.instagram_handle}</span>
</div> </div>
)} )}
<div className="flex justify-between"> <div className="flex items-center justify-between py-2">
<span className="text-gray-400">Member since</span> <span className="flex items-center gap-2 text-sm text-dim">
<span>{joinedDate}</span> <CalendarDays size={14} />
Member since
</span>
<span className="text-sm text-foreground">{joinedDate}</span>
</div> </div>
</div> </div>
<Separator /> <Button
variant="outline"
<Button variant="destructive" className="w-full" onClick={handleLogout}> className="w-full border-destructive/30 text-destructive hover:bg-destructive/10 hover:border-destructive/50"
onClick={handleLogout}
>
<LogOut size={16} />
Sign out Sign out
</Button> </Button>
</div> </div>

View File

@@ -2,25 +2,46 @@
import { useMyRegistrations } from "@/hooks/useRegistrations"; import { useMyRegistrations } from "@/hooks/useRegistrations";
import { RegistrationCard } from "@/components/registrations/RegistrationCard"; import { RegistrationCard } from "@/components/registrations/RegistrationCard";
import { ListChecks } from "lucide-react";
export default function RegistrationsPage() { export default function RegistrationsPage() {
const { data, isLoading, error } = useMyRegistrations(); const { data, isLoading, error } = useMyRegistrations();
if (isLoading) return <div className="flex justify-center py-20 text-gray-400">Loading</div>; if (isLoading) {
if (error) return <p className="text-center text-red-500 py-20">Failed to load registrations.</p>; return (
if (!data?.length) return ( <div className="flex justify-center py-20">
<div className="text-center py-20 text-gray-400"> <div className="relative h-8 w-8">
<p className="text-4xl mb-3">📋</p> <div className="absolute inset-0 rounded-full border-2 border-rose-accent/20" />
<p>No registrations yet.</p> <div className="absolute inset-0 rounded-full border-2 border-transparent border-t-rose-accent animate-spin" />
</div> </div>
); </div>
);
}
if (error) return <p className="text-center text-destructive py-20">Failed to load registrations.</p>;
if (!data?.length) {
return (
<div className="text-center py-20 animate-fade-in">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-surface-elevated border border-border/40">
<ListChecks className="h-7 w-7 text-dim" />
</div>
<p className="text-muted-foreground">No registrations yet.</p>
</div>
);
}
return ( return (
<div> <div className="animate-fade-in">
<h1 className="mb-6 text-2xl font-bold text-gray-900">My Registrations</h1> <div className="mb-8">
<h1 className="font-display text-4xl font-bold tracking-wide">My Registrations</h1>
<p className="mt-1 text-muted-foreground">Track your championship progress</p>
</div>
<div className="space-y-3 max-w-2xl"> <div className="space-y-3 max-w-2xl">
{data.map((r) => ( {data.map((r, i) => (
<RegistrationCard key={r.id} registration={r} /> <div key={r.id} className={`animate-fade-in-up stagger-${Math.min(i + 1, 9)}`}>
<RegistrationCard registration={r} />
</div>
))} ))}
</div> </div>
</div> </div>

View File

@@ -1,7 +1,38 @@
export default function AuthLayout({ children }: { children: React.ReactNode }) { export default function AuthLayout({ children }: { children: React.ReactNode }) {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-violet-50 to-purple-100 p-4"> <div className="relative flex min-h-screen items-center justify-center overflow-hidden p-4">
<div className="w-full max-w-md">{children}</div> {/* Gradient mesh background */}
<div className="fixed inset-0 bg-background bg-mesh-strong" />
{/* Decorative flowing lines — pole dance silhouette abstraction */}
<svg
className="fixed inset-0 h-full w-full opacity-[0.04]"
viewBox="0 0 1200 800"
fill="none"
preserveAspectRatio="xMidYMid slice"
>
<path
d="M-100,400 C100,200 300,600 500,350 C700,100 900,500 1100,300 C1300,100 1400,400 1400,400"
stroke="url(#line-grad)"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M-100,500 C200,300 400,700 600,450 C800,200 1000,600 1300,350"
stroke="url(#line-grad)"
strokeWidth="1.5"
strokeLinecap="round"
/>
<defs>
<linearGradient id="line-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#E91E63" />
<stop offset="50%" stopColor="#9C27B0" />
<stop offset="100%" stopColor="#D4A843" />
</linearGradient>
</defs>
</svg>
<div className="relative z-10 w-full max-w-md animate-fade-in-up">{children}</div>
</div> </div>
); );
} }

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { Eye, EyeOff } from "lucide-react";
import { useLoginForm } from "@/hooks/useAuthForms"; import { useLoginForm } from "@/hooks/useAuthForms";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -8,37 +9,77 @@ import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
export default function LoginPage() { export default function LoginPage() {
const { email, setEmail, password, setPassword, error, isLoading, submit } = useLoginForm(); const { email, setEmail, password, setPassword, showPassword, setShowPassword, error, isLoading, submit } = useLoginForm();
return ( return (
<Card> <Card className="glass-strong glow-rose overflow-hidden">
<CardHeader className="text-center"> <CardHeader className="text-center pb-2">
<div className="mx-auto mb-2 text-4xl">🏆</div> <div className="mx-auto mb-4 h-px w-16 bg-gradient-to-r from-transparent via-rose-accent to-transparent" />
<CardTitle className="text-2xl">Welcome back</CardTitle> <CardTitle className="font-display text-3xl font-semibold tracking-wide">
<CardDescription>Sign in to your account</CardDescription> Welcome back
</CardTitle>
<CardDescription className="text-muted-foreground">
Sign in to your account
</CardDescription>
</CardHeader> </CardHeader>
<form onSubmit={submit}> <form onSubmit={submit}>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{error && <p className="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>} {error && (
<p className="rounded-lg bg-destructive/10 border border-destructive/20 px-3 py-2 text-sm text-destructive">
{error}
</p>
)}
<div className="space-y-1"> <div className="space-y-2">
<Label htmlFor="email">Email</Label> <Label htmlFor="email" className="text-xs uppercase tracking-widest text-dim">
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required /> Email
</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30 placeholder:text-dim"
placeholder="your@email.com"
/>
</div> </div>
<div className="space-y-1"> <div className="space-y-2">
<Label htmlFor="password">Password</Label> <Label htmlFor="password" className="text-xs uppercase tracking-widest text-dim">
<Input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required /> Password
</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="bg-surface border-border/60 pr-10 focus:border-rose-accent focus:ring-rose-accent/30"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-dim hover:text-foreground transition-colors"
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div> </div>
</CardContent> </CardContent>
<CardFooter className="flex flex-col gap-3"> <CardFooter className="flex flex-col gap-4 pt-2">
<Button type="submit" className="w-full bg-violet-600 hover:bg-violet-700" disabled={isLoading}> <Button
type="submit"
className="w-full bg-rose-accent hover:bg-rose-accent/90 text-white font-medium tracking-wide"
disabled={isLoading}
>
{isLoading ? "Signing in…" : "Sign in"} {isLoading ? "Signing in…" : "Sign in"}
</Button> </Button>
<p className="text-center text-sm text-gray-500"> <p className="text-center text-sm text-muted-foreground">
No account?{" "} No account?{" "}
<Link href="/register" className="font-medium text-violet-600 hover:underline"> <Link href="/register" className="font-medium text-rose-accent hover:text-rose-accent/80 transition-colors">
Register Register
</Link> </Link>
</p> </p>

View File

@@ -1,22 +1,27 @@
import Link from "next/link"; import Link from "next/link";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Clock } from "lucide-react";
export default function PendingPage() { export default function PendingPage() {
return ( return (
<Card className="text-center"> <Card className="glass-strong glow-purple text-center overflow-hidden">
<CardHeader> <CardHeader>
<div className="mx-auto mb-2 text-5xl"></div> <div className="mx-auto mb-3 flex h-14 w-14 items-center justify-center rounded-full bg-purple-accent/10 border border-purple-accent/20">
<CardTitle className="text-2xl">Awaiting approval</CardTitle> <Clock className="h-7 w-7 text-purple-accent" />
<CardDescription> </div>
<CardTitle className="font-display text-3xl font-semibold tracking-wide">
Awaiting approval
</CardTitle>
<CardDescription className="text-muted-foreground">
Your organizer account has been submitted. An admin will review it shortly. Your organizer account has been submitted. An admin will review it shortly.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="mb-6 text-sm text-gray-500"> <p className="mb-6 text-sm text-dim">
Once approved you can log in and start creating championships. Once approved you can log in and start creating championships.
</p> </p>
<Button asChild variant="outline" className="w-full"> <Button asChild variant="outline" className="w-full border-border/60 hover:bg-surface-hover">
<Link href="/login">Back to login</Link> <Link href="/login">Back to login</Link>
</Button> </Button>
</CardContent> </CardContent>

View File

@@ -11,16 +11,24 @@ export default function RegisterPage() {
const { role, setRole, form, update, error, isLoading, submit } = useRegisterForm(); const { role, setRole, form, update, error, isLoading, submit } = useRegisterForm();
return ( return (
<Card> <Card className="glass-strong glow-rose overflow-hidden">
<CardHeader className="text-center"> <CardHeader className="text-center pb-2">
<div className="mx-auto mb-2 text-4xl">🏅</div> <div className="mx-auto mb-4 h-px w-16 bg-gradient-to-r from-transparent via-purple-accent to-transparent" />
<CardTitle className="text-2xl">Create account</CardTitle> <CardTitle className="font-display text-3xl font-semibold tracking-wide">
<CardDescription>Join the pole dance community</CardDescription> Create account
</CardTitle>
<CardDescription className="text-muted-foreground">
Join the pole dance community
</CardDescription>
</CardHeader> </CardHeader>
<form onSubmit={submit}> <form onSubmit={submit}>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{error && <p className="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>} {error && (
<p className="rounded-lg bg-destructive/10 border border-destructive/20 px-3 py-2 text-sm text-destructive">
{error}
</p>
)}
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{(["member", "organizer"] as const).map((r) => ( {(["member", "organizer"] as const).map((r) => (
@@ -28,11 +36,13 @@ export default function RegisterPage() {
key={r} key={r}
type="button" type="button"
onClick={() => setRole(r)} onClick={() => setRole(r)}
className={`rounded-lg border-2 p-3 text-sm font-medium transition-colors ${ className={`rounded-xl border-2 p-3 text-sm font-medium transition-all duration-200 ${
role === r ? "border-violet-600 bg-violet-50 text-violet-700" : "border-gray-200 text-gray-600 hover:border-gray-300" role === r
? "border-rose-accent bg-rose-accent/10 text-foreground glow-rose"
: "border-border/40 text-muted-foreground hover:border-border hover:text-foreground"
}`} }`}
> >
{r === "member" ? "🏅 Athlete" : "🏆 Organizer"} {r === "member" ? "Athlete" : "Organizer"}
</button> </button>
))} ))}
</div> </div>
@@ -54,13 +64,17 @@ export default function RegisterPage() {
)} )}
</CardContent> </CardContent>
<CardFooter className="flex flex-col gap-3"> <CardFooter className="flex flex-col gap-4 pt-2">
<Button type="submit" className="w-full bg-violet-600 hover:bg-violet-700" disabled={isLoading}> <Button
type="submit"
className="w-full bg-rose-accent hover:bg-rose-accent/90 text-white font-medium tracking-wide"
disabled={isLoading}
>
{isLoading ? "Creating…" : role === "member" ? "Create account" : "Submit for approval"} {isLoading ? "Creating…" : role === "member" ? "Create account" : "Submit for approval"}
</Button> </Button>
<p className="text-center text-sm text-gray-500"> <p className="text-center text-sm text-muted-foreground">
Have an account?{" "} Have an account?{" "}
<Link href="/login" className="font-medium text-violet-600 hover:underline"> <Link href="/login" className="font-medium text-rose-accent hover:text-rose-accent/80 transition-colors">
Sign in Sign in
</Link> </Link>
</p> </p>

View File

@@ -1,13 +1,17 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@import "shadcn/tailwind.css"; @import "shadcn/tailwind.css";
@import "../styles/theme.css";
@import "../styles/utilities.css";
@import "../styles/animations.css";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans); --font-sans: var(--font-outfit);
--font-display: var(--font-cormorant);
--font-mono: var(--font-geist-mono); --font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
@@ -45,75 +49,17 @@
--radius-2xl: calc(var(--radius) + 8px); --radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px); --radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px); --radius-4xl: calc(var(--radius) + 16px);
} --color-rose-accent: #E91E63;
--color-rose-soft: rgba(233, 30, 99, 0.15);
:root { --color-rose-glow: rgba(233, 30, 99, 0.25);
--radius: 0.625rem; --color-purple-accent: #9C27B0;
--background: oklch(1 0 0); --color-purple-soft: rgba(156, 39, 176, 0.15);
--foreground: oklch(0.145 0 0); --color-gold-accent: #D4A843;
--card: oklch(1 0 0); --color-gold-soft: rgba(212, 168, 67, 0.15);
--card-foreground: oklch(0.145 0 0); --color-surface: #0E0D18;
--popover: oklch(1 0 0); --color-surface-elevated: #15142A;
--popover-foreground: oklch(0.145 0 0); --color-surface-hover: #1C1B35;
--primary: oklch(0.205 0 0); --color-dim: #5C5880;
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
} }
@layer base { @layer base {

View File

@@ -1,9 +1,21 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist } from "next/font/google"; import { Cormorant_Garamond, Outfit } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { Providers } from "./providers"; import { Providers } from "./providers";
const geist = Geist({ subsets: ["latin"], variable: "--font-geist-sans" }); const cormorant = Cormorant_Garamond({
subsets: ["latin", "cyrillic"],
weight: ["400", "600", "700"],
variable: "--font-cormorant",
display: "swap",
});
const outfit = Outfit({
subsets: ["latin"],
weight: ["300", "400", "500", "600", "700"],
variable: "--font-outfit",
display: "swap",
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Pole Dance Championships", title: "Pole Dance Championships",
@@ -13,7 +25,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
<body className={`${geist.variable} font-sans antialiased bg-gray-50`}> <body className={`${cormorant.variable} ${outfit.variable} font-sans antialiased`}>
<Providers>{children}</Providers> <Providers>{children}</Providers>
</body> </body>
</html> </html>

View File

@@ -14,8 +14,14 @@ function AuthInitializer({ children }: { children: React.ReactNode }) {
if (!isInitialized) { if (!isInitialized) {
return ( return (
<div className="flex h-screen items-center justify-center"> <div className="flex h-screen items-center justify-center bg-background">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-violet-600 border-t-transparent" /> <div className="flex flex-col items-center gap-4 animate-fade-in">
<div className="relative h-10 w-10">
<div className="absolute inset-0 rounded-full border-2 border-rose-accent/20" />
<div className="absolute inset-0 rounded-full border-2 border-transparent border-t-rose-accent animate-spin" />
</div>
<p className="text-sm text-muted-foreground tracking-wide">Loading</p>
</div>
</div> </div>
); );
} }

View File

@@ -2,6 +2,7 @@ import { UserOut } from "@/types/user";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Building2, Phone, AtSign, CheckCircle, XCircle } from "lucide-react";
interface Props { interface Props {
user: UserOut; user: UserOut;
@@ -11,47 +12,69 @@ interface Props {
} }
const STATUS_DOT: Record<string, string> = { const STATUS_DOT: Record<string, string> = {
pending: "bg-orange-400", pending: "bg-gold-accent",
approved: "bg-green-500", approved: "bg-emerald-500",
rejected: "bg-red-500", rejected: "bg-destructive",
}; };
export function UserCard({ user, onApprove, onReject, isActing }: Props) { export function UserCard({ user, onApprove, onReject, isActing }: Props) {
const initials = user.full_name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2); const initials = user.full_name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2);
return ( return (
<Card> <Card className="border-border/40 bg-surface-elevated">
<CardContent className="p-4 flex gap-4 items-start"> <CardContent className="p-4 flex gap-4 items-start">
<Avatar className="h-10 w-10 shrink-0"> <Avatar className="h-10 w-10 shrink-0 border border-border/40">
<AvatarFallback className="bg-violet-100 text-violet-700 text-sm font-semibold"> <AvatarFallback className="bg-rose-accent/10 text-rose-accent text-sm font-semibold">
{initials} {initials}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="flex-1 min-w-0 space-y-1"> <div className="flex-1 min-w-0 space-y-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p className="font-semibold text-gray-900">{user.full_name}</p> <p className="font-semibold text-foreground">{user.full_name}</p>
<span className={`h-2 w-2 rounded-full shrink-0 ${STATUS_DOT[user.status] ?? "bg-gray-400"}`} /> <span className={`h-2 w-2 rounded-full shrink-0 ${STATUS_DOT[user.status] ?? "bg-dim"}`} />
</div> </div>
<p className="text-sm text-gray-500">{user.email}</p> <p className="text-sm text-muted-foreground">{user.email}</p>
{user.organization_name && <p className="text-sm text-gray-500">🏢 {user.organization_name}</p>} {user.organization_name && (
{user.phone && <p className="text-sm text-gray-500">📞 {user.phone}</p>} <p className="flex items-center gap-1.5 text-sm text-muted-foreground">
{user.instagram_handle && <p className="text-sm text-gray-500">📸 {user.instagram_handle}</p>} <Building2 size={12} className="text-dim" />
{user.organization_name}
</p>
)}
{user.phone && (
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
<Phone size={12} className="text-dim" />
{user.phone}
</p>
)}
{user.instagram_handle && (
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
<AtSign size={12} className="text-dim" />
{user.instagram_handle}
</p>
)}
</div> </div>
{user.status === "pending" && onApprove && onReject && ( {user.status === "pending" && onApprove && onReject && (
<div className="flex gap-2 shrink-0"> <div className="flex gap-2 shrink-0">
<Button size="sm" className="bg-green-600 hover:bg-green-700" disabled={isActing} onClick={() => onApprove(user.id)}> <Button
size="sm"
className="bg-emerald-600 hover:bg-emerald-500 text-white"
disabled={isActing}
onClick={() => onApprove(user.id)}
>
<CheckCircle size={14} />
Approve Approve
</Button> </Button>
<Button size="sm" variant="destructive" disabled={isActing} onClick={() => onReject(user.id)}> <Button size="sm" variant="destructive" disabled={isActing} onClick={() => onReject(user.id)}>
<XCircle size={14} />
Reject Reject
</Button> </Button>
</div> </div>
)} )}
{user.status !== "pending" && ( {user.status !== "pending" && (
<span className="text-xs text-gray-400 capitalize shrink-0">{user.status}</span> <span className="text-xs text-dim capitalize shrink-0">{user.status}</span>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -12,21 +12,21 @@ interface Props {
export function MemberFields({ full_name, email, password, phone, onChange }: Props) { export function MemberFields({ full_name, email, password, phone, onChange }: Props) {
return ( return (
<> <>
<div className="space-y-1"> <div className="space-y-2">
<Label htmlFor="full_name">Full name</Label> <Label htmlFor="full_name" className="text-xs uppercase tracking-widest text-dim">Full name</Label>
<Input id="full_name" value={full_name} onChange={(e) => onChange("full_name", e.target.value)} required /> <Input id="full_name" value={full_name} onChange={(e) => onChange("full_name", e.target.value)} required className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
</div> </div>
<div className="space-y-1"> <div className="space-y-2">
<Label htmlFor="email">Email</Label> <Label htmlFor="email" className="text-xs uppercase tracking-widest text-dim">Email</Label>
<Input id="email" type="email" value={email} onChange={(e) => onChange("email", e.target.value)} required /> <Input id="email" type="email" value={email} onChange={(e) => onChange("email", e.target.value)} required className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
</div> </div>
<div className="space-y-1"> <div className="space-y-2">
<Label htmlFor="password">Password</Label> <Label htmlFor="password" className="text-xs uppercase tracking-widest text-dim">Password</Label>
<Input id="password" type="password" value={password} onChange={(e) => onChange("password", e.target.value)} required /> <Input id="password" type="password" value={password} onChange={(e) => onChange("password", e.target.value)} required className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
</div> </div>
<div className="space-y-1"> <div className="space-y-2">
<Label htmlFor="phone">Phone (optional)</Label> <Label htmlFor="phone" className="text-xs uppercase tracking-widest text-dim">Phone (optional)</Label>
<Input id="phone" type="tel" value={phone} onChange={(e) => onChange("phone", e.target.value)} /> <Input id="phone" type="tel" value={phone} onChange={(e) => onChange("phone", e.target.value)} className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
</div> </div>
</> </>
); );

View File

@@ -1,5 +1,6 @@
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { AlertTriangle } from "lucide-react";
interface Props { interface Props {
organization_name: string; organization_name: string;
@@ -10,16 +11,17 @@ interface Props {
export function OrganizerFields({ organization_name, instagram_handle, onChange }: Props) { export function OrganizerFields({ organization_name, instagram_handle, onChange }: Props) {
return ( return (
<> <>
<p className="rounded-md bg-amber-50 px-3 py-2 text-xs text-amber-700"> <div className="flex items-center gap-2 rounded-lg bg-gold-soft border border-gold-accent/20 px-3 py-2 text-xs text-gold-accent">
<AlertTriangle size={14} className="shrink-0" />
Organizer accounts require admin approval before you can log in. Organizer accounts require admin approval before you can log in.
</p>
<div className="space-y-1">
<Label htmlFor="org">Organization name</Label>
<Input id="org" value={organization_name} onChange={(e) => onChange("organization_name", e.target.value)} required />
</div> </div>
<div className="space-y-1"> <div className="space-y-2">
<Label htmlFor="ig">Instagram (optional)</Label> <Label htmlFor="org" className="text-xs uppercase tracking-widest text-dim">Organization name</Label>
<Input id="ig" placeholder="@yourstudio" value={instagram_handle} onChange={(e) => onChange("instagram_handle", e.target.value)} /> <Input id="org" value={organization_name} onChange={(e) => onChange("organization_name", e.target.value)} required className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
</div>
<div className="space-y-2">
<Label htmlFor="ig" className="text-xs uppercase tracking-widest text-dim">Instagram (optional)</Label>
<Input id="ig" placeholder="@yourstudio" value={instagram_handle} onChange={(e) => onChange("instagram_handle", e.target.value)} className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30 placeholder:text-dim" />
</div> </div>
</> </>
); );

View File

@@ -2,6 +2,7 @@ import Link from "next/link";
import { Championship } from "@/types/championship"; import { Championship } from "@/types/championship";
import { StatusBadge } from "@/components/shared/StatusBadge"; import { StatusBadge } from "@/components/shared/StatusBadge";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { MapPin, Calendar, CreditCard } from "lucide-react";
interface Props { interface Props {
championship: Championship; championship: Championship;
@@ -14,22 +15,54 @@ export function ChampionshipCard({ championship: c }: Props) {
return ( return (
<Link href={`/championships/${c.id}`}> <Link href={`/championships/${c.id}`}>
<Card className="overflow-hidden transition-shadow hover:shadow-md cursor-pointer h-full"> <Card className="group overflow-hidden border-border/40 bg-surface-elevated hover:border-rose-accent/30 transition-all duration-300 cursor-pointer h-full hover:glow-rose">
{c.image_url ? ( {/* Image / gradient header */}
<img src={c.image_url} alt={c.title} className="h-40 w-full object-cover" /> <div className="relative overflow-hidden">
) : ( {c.image_url ? (
<div className="h-40 w-full bg-gradient-to-br from-violet-400 to-purple-600 flex items-center justify-center text-4xl"> <img
🏆 src={c.image_url}
</div> alt={c.title}
)} className="h-44 w-full object-cover transition-transform duration-500 group-hover:scale-105"
<CardContent className="p-4 space-y-2"> />
<div className="flex items-start justify-between gap-2"> ) : (
<h2 className="font-semibold text-gray-900 leading-tight">{c.title}</h2> <div className="h-44 w-full bg-gradient-to-br from-rose-accent/30 via-purple-accent/20 to-gold-accent/10 flex items-center justify-center">
<div className="text-5xl opacity-40 group-hover:opacity-60 transition-opacity duration-300 group-hover:animate-float">
💃
</div>
</div>
)}
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-surface-elevated via-transparent to-transparent" />
{/* Status badge floating on image */}
<div className="absolute top-3 right-3">
<StatusBadge status={c.status} /> <StatusBadge status={c.status} />
</div> </div>
{c.location && <p className="text-sm text-gray-500">📍 {c.location}</p>} </div>
{date && <p className="text-sm text-gray-500">📅 {date}</p>}
{c.entry_fee != null && <p className="text-sm font-medium text-violet-700">💳 {c.entry_fee} </p>} <CardContent className="p-4 space-y-2.5">
<h2 className="font-semibold text-foreground leading-tight group-hover:text-rose-accent transition-colors">
{c.title}
</h2>
<div className="space-y-1.5">
{c.location && (
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
<MapPin size={13} className="text-dim shrink-0" />
{c.location}
</p>
)}
{date && (
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
<Calendar size={13} className="text-dim shrink-0" />
{date}
</p>
)}
{c.entry_fee != null && (
<p className="flex items-center gap-1.5 text-sm font-medium text-gold-accent">
<CreditCard size={13} className="shrink-0" />
{c.entry_fee}
</p>
)}
</div>
</CardContent> </CardContent>
</Card> </Card>
</Link> </Link>

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter, usePathname } from "next/navigation";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { import {
@@ -11,16 +11,20 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Trophy, ListChecks, Shield, Menu } from "lucide-react";
import { useState } from "react";
const NAV_LINKS = [ const NAV_LINKS = [
{ href: "/championships", label: "Championships" }, { href: "/championships", label: "Championships", icon: Trophy },
{ href: "/registrations", label: "My Registrations" }, { href: "/registrations", label: "My Registrations", icon: ListChecks },
]; ];
export function Navbar() { export function Navbar() {
const router = useRouter(); const router = useRouter();
const pathname = usePathname();
const user = useAuth((s) => s.user); const user = useAuth((s) => s.user);
const logout = useAuth((s) => s.logout); const logout = useAuth((s) => s.logout);
const [mobileOpen, setMobileOpen] = useState(false);
async function handleLogout() { async function handleLogout() {
await logout(); await logout();
@@ -35,47 +39,115 @@ export function Navbar() {
.slice(0, 2) ?? "?"; .slice(0, 2) ?? "?";
return ( return (
<header className="sticky top-0 z-50 border-b bg-white"> <header className="sticky top-0 z-50 glass-strong">
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4"> <div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<Link href="/championships" className="text-lg font-bold text-violet-700"> <Link href="/championships" className="flex items-center gap-2 group">
🏆 DanceChamp <span className="text-gradient-rose font-display text-xl font-bold tracking-wide">
DanceChamp
</span>
</Link> </Link>
<nav className="hidden gap-4 text-sm font-medium text-gray-600 sm:flex">
{NAV_LINKS.map((link) => ( {/* Desktop nav */}
<Link key={link.href} href={link.href} className="hover:text-violet-700 transition-colors"> <nav className="hidden gap-1 sm:flex">
{link.label} {NAV_LINKS.map((link) => {
</Link> const active = pathname.startsWith(link.href);
))} return (
<Link
key={link.href}
href={link.href}
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-all duration-200 ${
active
? "bg-rose-accent/10 text-rose-accent"
: "text-muted-foreground hover:text-foreground hover:bg-surface-hover"
}`}
>
<link.icon size={15} />
{link.label}
</Link>
);
})}
{user?.role === "admin" && ( {user?.role === "admin" && (
<Link href="/admin" className="hover:text-violet-700 transition-colors"> <Link
href="/admin"
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-all duration-200 ${
pathname.startsWith("/admin")
? "bg-rose-accent/10 text-rose-accent"
: "text-muted-foreground hover:text-foreground hover:bg-surface-hover"
}`}
>
<Shield size={15} />
Admin Admin
</Link> </Link>
)} )}
</nav> </nav>
</div> </div>
<DropdownMenu> <div className="flex items-center gap-2">
<DropdownMenuTrigger asChild> {/* Mobile menu toggle */}
<button className="rounded-full focus:outline-none focus:ring-2 focus:ring-violet-500"> <button
<Avatar className="h-8 w-8 cursor-pointer"> onClick={() => setMobileOpen(!mobileOpen)}
<AvatarFallback className="bg-violet-100 text-violet-700 text-xs font-semibold"> className="rounded-lg p-1.5 text-muted-foreground hover:text-foreground hover:bg-surface-hover transition-colors sm:hidden"
{initials} >
</AvatarFallback> <Menu size={20} />
</Avatar> </button>
</button>
</DropdownMenuTrigger> <DropdownMenu>
<DropdownMenuContent align="end" className="w-44"> <DropdownMenuTrigger asChild>
<DropdownMenuItem asChild> <button className="rounded-full focus:outline-none focus:ring-2 focus:ring-rose-accent/50">
<Link href="/profile">Profile</Link> <Avatar className="h-8 w-8 cursor-pointer border border-rose-accent/30">
</DropdownMenuItem> <AvatarFallback className="bg-rose-accent/10 text-rose-accent text-xs font-semibold">
<DropdownMenuSeparator /> {initials}
<DropdownMenuItem onClick={handleLogout} className="text-red-600 focus:text-red-600"> </AvatarFallback>
Sign out </Avatar>
</DropdownMenuItem> </button>
</DropdownMenuContent> </DropdownMenuTrigger>
</DropdownMenu> <DropdownMenuContent align="end" className="w-44 glass-strong border-border/60">
<DropdownMenuItem asChild>
<Link href="/profile" className="text-foreground">Profile</Link>
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-border/40" />
<DropdownMenuItem onClick={handleLogout} className="text-destructive focus:text-destructive">
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div> </div>
{/* Mobile nav */}
{mobileOpen && (
<nav className="border-t border-border/30 px-4 pb-3 pt-2 sm:hidden animate-fade-in">
{NAV_LINKS.map((link) => {
const active = pathname.startsWith(link.href);
return (
<Link
key={link.href}
href={link.href}
onClick={() => setMobileOpen(false)}
className={`flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
active ? "text-rose-accent" : "text-muted-foreground hover:text-foreground"
}`}
>
<link.icon size={16} />
{link.label}
</Link>
);
})}
{user?.role === "admin" && (
<Link
href="/admin"
onClick={() => setMobileOpen(false)}
className={`flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
pathname.startsWith("/admin") ? "text-rose-accent" : "text-muted-foreground hover:text-foreground"
}`}
>
<Shield size={16} />
Admin
</Link>
)}
</nav>
)}
</header> </header>
); );
} }

View File

@@ -2,6 +2,7 @@ import Link from "next/link";
import { Registration } from "@/types/registration"; import { Registration } from "@/types/registration";
import { StatusBadge } from "@/components/shared/StatusBadge"; import { StatusBadge } from "@/components/shared/StatusBadge";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { MapPin, Calendar } from "lucide-react";
const STEPS = ["submitted", "form_submitted", "payment_pending", "payment_confirmed", "video_submitted", "accepted"]; const STEPS = ["submitted", "form_submitted", "payment_pending", "payment_confirmed", "video_submitted", "accepted"];
@@ -18,24 +19,38 @@ export function RegistrationCard({ registration: r }: Props) {
return ( return (
<Link href={`/championships/${r.championship_id}`}> <Link href={`/championships/${r.championship_id}`}>
<Card className="hover:shadow-md transition-shadow cursor-pointer"> <Card className="group border-border/40 bg-surface-elevated hover:border-rose-accent/30 transition-all duration-300 cursor-pointer hover:glow-rose">
<CardContent className="p-4 space-y-3"> <CardContent className="p-4 space-y-3">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div> <div className="space-y-1.5">
<p className="font-semibold text-gray-900">{r.championship_title ?? "Championship"}</p> <p className="font-semibold text-foreground group-hover:text-rose-accent transition-colors">
{r.championship_location && <p className="text-sm text-gray-500">📍 {r.championship_location}</p>} {r.championship_title ?? "Championship"}
{date && <p className="text-sm text-gray-500">📅 {date}</p>} </p>
{r.championship_location && (
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
<MapPin size={13} className="text-dim shrink-0" />
{r.championship_location}
</p>
)}
{date && (
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
<Calendar size={13} className="text-dim shrink-0" />
{date}
</p>
)}
</div> </div>
<StatusBadge status={r.status} type="registration" /> <StatusBadge status={r.status} type="registration" />
</div> </div>
{/* Progress dots */} {/* Progress bar */}
<div className="flex gap-1.5"> <div className="flex gap-1.5">
{STEPS.map((_, i) => ( {STEPS.map((_, i) => (
<div <div
key={i} key={i}
className={`h-2 flex-1 rounded-full ${ className={`h-1.5 flex-1 rounded-full transition-colors ${
i <= stepIndex ? "bg-violet-500" : "bg-gray-200" i <= stepIndex
? "bg-gradient-to-r from-rose-accent to-purple-accent"
: "bg-border/40"
}`} }`}
/> />
))} ))}

View File

@@ -1,4 +1,5 @@
import { Registration } from "@/types/registration"; import { Registration } from "@/types/registration";
import { Check, AlertTriangle } from "lucide-react";
const STEPS: { key: string; label: string }[] = [ const STEPS: { key: string; label: string }[] = [
{ key: "submitted", label: "Submitted" }, { key: "submitted", label: "Submitted" },
@@ -20,7 +21,8 @@ export function RegistrationTimeline({ registration }: Props) {
if (isRejected) { if (isRejected) {
return ( return (
<div className="rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700"> <div className="flex items-center gap-3 rounded-xl bg-destructive/10 border border-destructive/20 px-4 py-3 text-sm text-destructive">
<AlertTriangle size={16} className="shrink-0" />
Your registration was <strong>rejected</strong>. Your registration was <strong>rejected</strong>.
</div> </div>
); );
@@ -28,26 +30,34 @@ export function RegistrationTimeline({ registration }: Props) {
if (isWaitlisted) { if (isWaitlisted) {
return ( return (
<div className="rounded-lg bg-amber-50 px-4 py-3 text-sm text-amber-700"> <div className="flex items-center gap-3 rounded-xl bg-gold-soft border border-gold-accent/20 px-4 py-3 text-sm text-gold-accent">
<AlertTriangle size={16} className="shrink-0" />
You are on the <strong>waitlist</strong>. You are on the <strong>waitlist</strong>.
</div> </div>
); );
} }
return ( return (
<div className="space-y-2"> <div className="space-y-3">
<p className="text-sm font-medium text-gray-700">Registration progress</p> <p className="text-sm font-medium text-foreground">Registration progress</p>
<ol className="space-y-2"> <ol className="space-y-1">
{STEPS.map((step, i) => { {STEPS.map((step, i) => {
const done = i <= currentIndex; const done = i <= currentIndex;
const isCurrent = i === currentIndex;
return ( return (
<li key={step.key} className="flex items-center gap-3 text-sm"> <li key={step.key} className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm">
<span className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-xs font-bold ${ <span className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-bold transition-all ${
done ? "bg-violet-600 text-white" : "bg-gray-200 text-gray-400" done
}`}> ? "bg-rose-accent text-white"
{done ? "✓" : i + 1} : "bg-surface-elevated border border-border/40 text-dim"
} ${isCurrent ? "glow-rose" : ""}`}>
{done ? <Check size={13} /> : i + 1}
</span>
<span className={`${
done ? "text-foreground" : "text-dim"
} ${isCurrent ? "font-medium" : ""}`}>
{step.label}
</span> </span>
<span className={done ? "text-gray-900" : "text-gray-400"}>{step.label}</span>
</li> </li>
); );
})} })}

View File

@@ -1,21 +1,21 @@
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
const CHAMPIONSHIP_COLORS: Record<string, string> = { const CHAMPIONSHIP_COLORS: Record<string, string> = {
open: "bg-green-100 text-green-700", open: "bg-emerald-500/15 text-emerald-400 border-emerald-500/20",
closed: "bg-gray-100 text-gray-600", closed: "bg-surface-elevated text-dim border-border/40",
draft: "bg-yellow-100 text-yellow-700", draft: "bg-gold-soft text-gold-accent border-gold-accent/20",
completed: "bg-blue-100 text-blue-700", completed: "bg-blue-500/15 text-blue-400 border-blue-500/20",
}; };
const REGISTRATION_COLORS: Record<string, string> = { const REGISTRATION_COLORS: Record<string, string> = {
submitted: "bg-gray-100 text-gray-600", submitted: "bg-surface-elevated text-muted-foreground border-border/40",
form_submitted: "bg-yellow-100 text-yellow-700", form_submitted: "bg-gold-soft text-gold-accent border-gold-accent/20",
payment_pending: "bg-orange-100 text-orange-700", payment_pending: "bg-orange-500/15 text-orange-400 border-orange-500/20",
payment_confirmed: "bg-blue-100 text-blue-700", payment_confirmed: "bg-blue-500/15 text-blue-400 border-blue-500/20",
video_submitted: "bg-violet-100 text-violet-700", video_submitted: "bg-purple-soft text-purple-accent border-purple-accent/20",
accepted: "bg-green-100 text-green-700", accepted: "bg-emerald-500/15 text-emerald-400 border-emerald-500/20",
rejected: "bg-red-100 text-red-700", rejected: "bg-destructive/15 text-destructive border-destructive/20",
waitlisted: "bg-amber-100 text-amber-700", waitlisted: "bg-gold-soft text-gold-accent border-gold-accent/20",
}; };
interface Props { interface Props {
@@ -25,8 +25,10 @@ interface Props {
export function StatusBadge({ status, type = "championship" }: Props) { export function StatusBadge({ status, type = "championship" }: Props) {
const map = type === "championship" ? CHAMPIONSHIP_COLORS : REGISTRATION_COLORS; const map = type === "championship" ? CHAMPIONSHIP_COLORS : REGISTRATION_COLORS;
const color = map[status] ?? "bg-gray-100 text-gray-600"; const color = map[status] ?? "bg-surface-elevated text-dim border-border/40";
return ( return (
<Badge className={`${color} border-0 capitalize`}>{status.replace("_", " ")}</Badge> <Badge className={`${color} border capitalize text-[11px] tracking-wide`}>
{status.replace("_", " ")}
</Badge>
); );
} }

View File

@@ -0,0 +1,62 @@
/* ─── Keyframes ─── */
@keyframes fade-in-up {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-in-left {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 20px rgba(233, 30, 99, 0.2); }
50% { box-shadow: 0 0 40px rgba(233, 30, 99, 0.4); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@keyframes spin-slow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* ─── Animation utilities ─── */
.animate-fade-in-up { animation: fade-in-up 0.5s ease-out both; }
.animate-fade-in { animation: fade-in 0.4s ease-out both; }
.animate-slide-in-left { animation: slide-in-left 0.5s ease-out both; }
.animate-pulse-glow { animation: pulse-glow 3s ease-in-out infinite; }
.animate-float { animation: float 4s ease-in-out infinite; }
.animate-spin-slow { animation: spin-slow 8s linear infinite; }
.animate-shimmer {
background-size: 200% 100%;
animation: shimmer 2s linear infinite;
}
/* ─── Stagger delays ─── */
.stagger-1 { animation-delay: 0.05s; }
.stagger-2 { animation-delay: 0.1s; }
.stagger-3 { animation-delay: 0.15s; }
.stagger-4 { animation-delay: 0.2s; }
.stagger-5 { animation-delay: 0.25s; }
.stagger-6 { animation-delay: 0.3s; }
.stagger-7 { animation-delay: 0.35s; }
.stagger-8 { animation-delay: 0.4s; }
.stagger-9 { animation-delay: 0.45s; }

38
web/src/styles/theme.css Normal file
View File

@@ -0,0 +1,38 @@
/* ──────────────────────────────────────────
DARK LUXURY THEME — CSS Variables
────────────────────────────────────────── */
:root {
--radius: 0.75rem;
--background: #07060E;
--foreground: #F2EFFF;
--card: #12111F;
--card-foreground: #F2EFFF;
--popover: #15142A;
--popover-foreground: #F2EFFF;
--primary: #E91E63;
--primary-foreground: #FFFFFF;
--secondary: #1A1935;
--secondary-foreground: #C8C4E0;
--muted: #15142A;
--muted-foreground: #7B78A0;
--accent: #1F1E38;
--accent-foreground: #E8E5FF;
--destructive: #FF1744;
--border: #232040;
--input: #1A1935;
--ring: #E91E63;
--chart-1: #E91E63;
--chart-2: #9C27B0;
--chart-3: #D4A843;
--chart-4: #40C4FF;
--chart-5: #00E676;
--sidebar: #0A091A;
--sidebar-foreground: #F2EFFF;
--sidebar-primary: #E91E63;
--sidebar-primary-foreground: #FFFFFF;
--sidebar-accent: #1A1935;
--sidebar-accent-foreground: #F2EFFF;
--sidebar-border: #232040;
--sidebar-ring: #E91E63;
}

View File

@@ -0,0 +1,108 @@
/* ─── Glass morphism ─── */
.glass {
background: rgba(18, 17, 31, 0.7);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(35, 32, 64, 0.6);
}
.glass-strong {
background: rgba(18, 17, 31, 0.85);
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);
border: 1px solid rgba(35, 32, 64, 0.8);
}
/* ─── Glow effects ─── */
.glow-rose {
box-shadow: 0 0 20px rgba(233, 30, 99, 0.15), 0 0 60px rgba(233, 30, 99, 0.05);
}
.glow-rose-strong {
box-shadow: 0 0 30px rgba(233, 30, 99, 0.25), 0 0 80px rgba(233, 30, 99, 0.1);
}
.glow-purple {
box-shadow: 0 0 20px rgba(156, 39, 176, 0.15), 0 0 60px rgba(156, 39, 176, 0.05);
}
.glow-gold {
box-shadow: 0 0 20px rgba(212, 168, 67, 0.15), 0 0 60px rgba(212, 168, 67, 0.05);
}
/* ─── Gradient border ─── */
.gradient-border {
position: relative;
}
.gradient-border::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: linear-gradient(135deg, rgba(233, 30, 99, 0.4), rgba(156, 39, 176, 0.2), rgba(212, 168, 67, 0.3));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
/* ─── Text gradients ─── */
.text-gradient-rose {
background: linear-gradient(135deg, #E91E63, #F48FB1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.text-gradient-gold {
background: linear-gradient(135deg, #D4A843, #F5D58E);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.text-gradient-mixed {
background: linear-gradient(135deg, #E91E63, #9C27B0, #D4A843);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ─── Background meshes ─── */
.bg-mesh {
background:
radial-gradient(ellipse 80% 50% at 20% 40%, rgba(233, 30, 99, 0.08), transparent),
radial-gradient(ellipse 60% 40% at 80% 20%, rgba(156, 39, 176, 0.06), transparent),
radial-gradient(ellipse 50% 60% at 50% 80%, rgba(212, 168, 67, 0.04), transparent);
}
.bg-mesh-strong {
background:
radial-gradient(ellipse 80% 50% at 20% 40%, rgba(233, 30, 99, 0.12), transparent),
radial-gradient(ellipse 60% 40% at 80% 20%, rgba(156, 39, 176, 0.10), transparent),
radial-gradient(ellipse 50% 60% at 50% 80%, rgba(212, 168, 67, 0.06), transparent);
}
/* ─── Scrollbar ─── */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #232040;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #2E2B55;
}