From 6fbd0326fa4c949f095e05a08bea681da99af4eb Mon Sep 17 00:00:00 2001 From: Dianaka123 Date: Fri, 27 Feb 2026 13:56:45 +0300 Subject: [PATCH] =?UTF-8?q?POL-125:=20Frontend=20visual=20upgrade=20?= =?UTF-8?q?=E2=80=94=20dark=20luxury=20pole=20dance=20theme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- web/src/app/(app)/admin/page.tsx | 80 ++++++---- web/src/app/(app)/championships/[id]/page.tsx | 93 ++++++++---- web/src/app/(app)/championships/page.tsx | 42 +++++- web/src/app/(app)/layout.tsx | 4 +- web/src/app/(app)/profile/page.tsx | 84 +++++++---- web/src/app/(app)/registrations/page.tsx | 45 ++++-- web/src/app/(auth)/layout.tsx | 35 ++++- web/src/app/(auth)/login/page.tsx | 75 +++++++--- web/src/app/(auth)/pending/page.tsx | 17 ++- web/src/app/(auth)/register/page.tsx | 40 +++-- web/src/app/globals.css | 88 +++-------- web/src/app/layout.tsx | 18 ++- web/src/app/providers.tsx | 10 +- web/src/components/admin/UserCard.tsx | 51 +++++-- web/src/components/auth/MemberFields.tsx | 24 +-- web/src/components/auth/OrganizerFields.tsx | 18 ++- .../championships/ChampionshipCard.tsx | 61 ++++++-- web/src/components/layout/Navbar.tsx | 138 +++++++++++++----- .../registrations/RegistrationCard.tsx | 31 +++- .../registrations/RegistrationTimeline.tsx | 32 ++-- web/src/components/shared/StatusBadge.tsx | 30 ++-- web/src/styles/animations.css | 62 ++++++++ web/src/styles/theme.css | 38 +++++ web/src/styles/utilities.css | 108 ++++++++++++++ 24 files changed, 882 insertions(+), 342 deletions(-) create mode 100644 web/src/styles/animations.css create mode 100644 web/src/styles/theme.css create mode 100644 web/src/styles/utilities.css diff --git a/web/src/app/(app)/admin/page.tsx b/web/src/app/(app)/admin/page.tsx index 408f868..5fde10f 100644 --- a/web/src/app/(app)/admin/page.tsx +++ b/web/src/app/(app)/admin/page.tsx @@ -1,11 +1,11 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useUsers, useUserActions } from "@/hooks/useUsers"; import { useAuth } from "@/hooks/useAuth"; import { UserCard } from "@/components/admin/UserCard"; import { useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { Shield } from "lucide-react"; type Filter = "pending" | "all"; @@ -20,47 +20,63 @@ export default function AdminPage() { if (user && user.role !== "admin") router.replace("/championships"); }, [user, router]); - if (isLoading) return
Loading…
; - if (error) return

Failed to load users.

; + if (isLoading) { + return ( +
+
+
+
+
+
+ ); + } + + if (error) return

Failed to load users.

; const pending = data?.filter((u) => u.status === "pending") ?? []; const shown = filter === "pending" ? pending : (data ?? []); return ( -
-

User Management

+
+
+

User Management

+

Review and manage user accounts

+
-
- - +
+ {(["pending", "all"] as const).map((f) => ( + + ))}
{shown.length === 0 ? ( -

No users in this category.

+
+
+ +
+

No users in this category.

+
) : (
- {shown.map((u) => ( - approve.mutate(id)} - onReject={(id) => reject.mutate(id)} - isActing={approve.isPending || reject.isPending} - /> + {shown.map((u, i) => ( +
+ approve.mutate(id)} + onReject={(id) => reject.mutate(id)} + isActing={approve.isPending || reject.isPending} + /> +
))}
)} diff --git a/web/src/app/(app)/championships/[id]/page.tsx b/web/src/app/(app)/championships/[id]/page.tsx index 557eab2..d5ddaf9 100644 --- a/web/src/app/(app)/championships/[id]/page.tsx +++ b/web/src/app/(app)/championships/[id]/page.tsx @@ -4,22 +4,30 @@ import { use } from "react"; import { useChampionship } from "@/hooks/useChampionships"; import { useMyRegistrations } from "@/hooks/useRegistrations"; import { useRegisterForChampionship } from "@/hooks/useRegistrations"; -import { useAuth } from "@/hooks/useAuth"; import { RegistrationTimeline } from "@/components/registrations/RegistrationTimeline"; import { StatusBadge } from "@/components/shared/StatusBadge"; import { Button } from "@/components/ui/button"; 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 }> }) { const { id } = use(params); - const user = useAuth((s) => s.user); - const { data: championship, isLoading, error } = useChampionship(id); const { data: myRegs } = useMyRegistrations(); const registerMutation = useRegisterForChampionship(id); - if (isLoading) return
Loading…
; - if (error || !championship) return

Championship not found.

; + if (isLoading) { + return ( +
+
+
+
+
+
+ ); + } + + if (error || !championship) return

Championship not found.

; const myReg = myRegs?.find((r) => r.championship_id === id); const canRegister = championship.status === "open" && !myReg; @@ -29,51 +37,77 @@ export default function ChampionshipDetailPage({ params }: { params: Promise<{ i : null; return ( -
+
{/* Header image */} - {championship.image_url ? ( - {championship.title} - ) : ( -
- πŸ† -
- )} +
+ {championship.image_url ? ( + {championship.title} + ) : ( +
+ πŸ’ƒ +
+ )} +
+
{/* Title + status */}
-

{championship.title}

- {championship.subtitle &&

{championship.subtitle}

} +

{championship.title}

+ {championship.subtitle &&

{championship.subtitle}

}
- {/* Details */} -
- {championship.location &&

πŸ“ {championship.location}

} - {championship.venue &&

πŸ› {championship.venue}

} - {eventDate &&

πŸ“… {eventDate}

} - {championship.entry_fee != null &&

πŸ’³ Entry fee: {championship.entry_fee} β‚½

} + {/* Details grid */} +
+ {championship.location && ( +
+ + {championship.location} +
+ )} + {championship.venue && ( +
+ + {championship.venue} +
+ )} + {eventDate && ( +
+ + {eventDate} +
+ )} + {championship.entry_fee != null && ( +
+ + Entry fee: {championship.entry_fee} β‚½ +
+ )} {championship.video_max_duration != null && ( -

🎬 Max video: {Math.floor(championship.video_max_duration / 60)}m {championship.video_max_duration % 60}s

+
+ + Max video: {Math.floor(championship.video_max_duration / 60)}m {championship.video_max_duration % 60}s +
)}
{championship.description && ( <> - -

{championship.description}

+ +

{championship.description}

)} - + {/* Registration section */} {myReg && } {canRegister && (
diff --git a/web/src/app/(app)/championships/page.tsx b/web/src/app/(app)/championships/page.tsx index b325bcf..b41ccae 100644 --- a/web/src/app/(app)/championships/page.tsx +++ b/web/src/app/(app)/championships/page.tsx @@ -2,20 +2,46 @@ import { useChampionships } from "@/hooks/useChampionships"; import { ChampionshipCard } from "@/components/championships/ChampionshipCard"; +import { Trophy } from "lucide-react"; export default function ChampionshipsPage() { const { data, isLoading, error } = useChampionships(); - if (isLoading) return
Loading…
; - if (error) return

Failed to load championships.

; - if (!data?.length) return

No championships yet.

; + if (isLoading) { + return ( +
+
+
+
+
+
+ ); + } + + if (error) return

Failed to load championships.

; + + if (!data?.length) { + return ( +
+
+ +
+

No championships yet.

+
+ ); + } return ( -
-

Championships

-
- {data.map((c) => ( - +
+
+

Championships

+

Browse upcoming competitions

+
+
+ {data.map((c, i) => ( +
+ +
))}
diff --git a/web/src/app/(app)/layout.tsx b/web/src/app/(app)/layout.tsx index 6a727ed..067c0c5 100644 --- a/web/src/app/(app)/layout.tsx +++ b/web/src/app/(app)/layout.tsx @@ -2,9 +2,9 @@ import { Navbar } from "@/components/layout/Navbar"; export default function AppLayout({ children }: { children: React.ReactNode }) { return ( - <> +
{children}
- +
); } diff --git a/web/src/app/(app)/profile/page.tsx b/web/src/app/(app)/profile/page.tsx index b68d96b..303aa8c 100644 --- a/web/src/app/(app)/profile/page.tsx +++ b/web/src/app/(app)/profile/page.tsx @@ -6,11 +6,12 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; +import { Phone, Building2, AtSign, CalendarDays, LogOut } from "lucide-react"; const ROLE_COLORS: Record = { - admin: "bg-red-100 text-red-700", - organizer: "bg-violet-100 text-violet-700", - member: "bg-green-100 text-green-700", + admin: "bg-destructive/15 text-destructive border-destructive/20", + organizer: "bg-purple-soft text-purple-accent border-purple-accent/20", + member: "bg-rose-soft text-rose-accent border-rose-accent/20", }; export default function ProfilePage() { @@ -29,52 +30,73 @@ export default function ProfilePage() { } return ( -
-
- - - {initials} - - -
-

{user.full_name}

-

{user.email}

+
+
+ {/* Avatar with gradient ring */} +
+
+ + + {initials} + +
- + +
+

{user.full_name}

+

{user.email}

+
+ + {user.role}
- + -
+
{user.phone && ( -
- Phone - {user.phone} +
+ + + Phone + + {user.phone}
)} {user.organization_name && ( -
- Organization - {user.organization_name} +
+ + + Organization + + {user.organization_name}
)} {user.instagram_handle && ( -
- Instagram - {user.instagram_handle} +
+ + + AtSign + + {user.instagram_handle}
)} -
- Member since - {joinedDate} +
+ + + Member since + + {joinedDate}
- - -
diff --git a/web/src/app/(app)/registrations/page.tsx b/web/src/app/(app)/registrations/page.tsx index 6f25d7b..c55efb1 100644 --- a/web/src/app/(app)/registrations/page.tsx +++ b/web/src/app/(app)/registrations/page.tsx @@ -2,25 +2,46 @@ import { useMyRegistrations } from "@/hooks/useRegistrations"; import { RegistrationCard } from "@/components/registrations/RegistrationCard"; +import { ListChecks } from "lucide-react"; export default function RegistrationsPage() { const { data, isLoading, error } = useMyRegistrations(); - if (isLoading) return
Loading…
; - if (error) return

Failed to load registrations.

; - if (!data?.length) return ( -
-

πŸ“‹

-

No registrations yet.

-
- ); + if (isLoading) { + return ( +
+
+
+
+
+
+ ); + } + + if (error) return

Failed to load registrations.

; + + if (!data?.length) { + return ( +
+
+ +
+

No registrations yet.

+
+ ); + } return ( -
-

My Registrations

+
+
+

My Registrations

+

Track your championship progress

+
- {data.map((r) => ( - + {data.map((r, i) => ( +
+ +
))}
diff --git a/web/src/app/(auth)/layout.tsx b/web/src/app/(auth)/layout.tsx index 980fa94..36f7a53 100644 --- a/web/src/app/(auth)/layout.tsx +++ b/web/src/app/(auth)/layout.tsx @@ -1,7 +1,38 @@ export default function AuthLayout({ children }: { children: React.ReactNode }) { return ( -
-
{children}
+
+ {/* Gradient mesh background */} +
+ + {/* Decorative flowing lines β€” pole dance silhouette abstraction */} + + + + + + + + + + + + +
{children}
); } diff --git a/web/src/app/(auth)/login/page.tsx b/web/src/app/(auth)/login/page.tsx index 62fe7d7..04b41cd 100644 --- a/web/src/app/(auth)/login/page.tsx +++ b/web/src/app/(auth)/login/page.tsx @@ -1,6 +1,7 @@ "use client"; import Link from "next/link"; +import { Eye, EyeOff } from "lucide-react"; import { useLoginForm } from "@/hooks/useAuthForms"; import { Button } from "@/components/ui/button"; 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"; 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 ( - - -
πŸ†
- Welcome back - Sign in to your account + + +
+ + Welcome back + + + Sign in to your account +
- {error &&

{error}

} + {error && ( +

+ {error} +

+ )} -
- - setEmail(e.target.value)} required /> +
+ + 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" + />
-
- - setPassword(e.target.value)} required /> +
+ +
+ setPassword(e.target.value)} + required + className="bg-surface border-border/60 pr-10 focus:border-rose-accent focus:ring-rose-accent/30" + /> + +
- - -

+

No account?{" "} - + Register

diff --git a/web/src/app/(auth)/pending/page.tsx b/web/src/app/(auth)/pending/page.tsx index 5e8b81f..33f4678 100644 --- a/web/src/app/(auth)/pending/page.tsx +++ b/web/src/app/(auth)/pending/page.tsx @@ -1,22 +1,27 @@ import Link from "next/link"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +import { Clock } from "lucide-react"; export default function PendingPage() { return ( - + -
⏳
- Awaiting approval - +
+ +
+ + Awaiting approval + + Your organizer account has been submitted. An admin will review it shortly.
-

+

Once approved you can log in and start creating championships.

-
diff --git a/web/src/app/(auth)/register/page.tsx b/web/src/app/(auth)/register/page.tsx index 328185a..f133703 100644 --- a/web/src/app/(auth)/register/page.tsx +++ b/web/src/app/(auth)/register/page.tsx @@ -11,16 +11,24 @@ export default function RegisterPage() { const { role, setRole, form, update, error, isLoading, submit } = useRegisterForm(); return ( - - -
πŸ…
- Create account - Join the pole dance community + + +
+ + Create account + + + Join the pole dance community + - {error &&

{error}

} + {error && ( +

+ {error} +

+ )}
{(["member", "organizer"] as const).map((r) => ( @@ -28,11 +36,13 @@ export default function RegisterPage() { key={r} type="button" onClick={() => setRole(r)} - className={`rounded-lg border-2 p-3 text-sm font-medium transition-colors ${ - role === r ? "border-violet-600 bg-violet-50 text-violet-700" : "border-gray-200 text-gray-600 hover:border-gray-300" + className={`rounded-xl border-2 p-3 text-sm font-medium transition-all duration-200 ${ + 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"} ))}
@@ -54,13 +64,17 @@ export default function RegisterPage() { )}
- - -

+

Have an account?{" "} - + Sign in

diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 382ca14..b26a405 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -1,13 +1,17 @@ @import "tailwindcss"; @import "tw-animate-css"; @import "shadcn/tailwind.css"; +@import "../styles/theme.css"; +@import "../styles/utilities.css"; +@import "../styles/animations.css"; @custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --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); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); @@ -45,75 +49,17 @@ --radius-2xl: calc(var(--radius) + 8px); --radius-3xl: calc(var(--radius) + 12px); --radius-4xl: calc(var(--radius) + 16px); -} - -:root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --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); + --color-rose-accent: #E91E63; + --color-rose-soft: rgba(233, 30, 99, 0.15); + --color-rose-glow: rgba(233, 30, 99, 0.25); + --color-purple-accent: #9C27B0; + --color-purple-soft: rgba(156, 39, 176, 0.15); + --color-gold-accent: #D4A843; + --color-gold-soft: rgba(212, 168, 67, 0.15); + --color-surface: #0E0D18; + --color-surface-elevated: #15142A; + --color-surface-hover: #1C1B35; + --color-dim: #5C5880; } @layer base { @@ -123,4 +69,4 @@ body { @apply bg-background text-foreground; } -} \ No newline at end of file +} diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index a017e8d..0e460f9 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,9 +1,21 @@ import type { Metadata } from "next"; -import { Geist } from "next/font/google"; +import { Cormorant_Garamond, Outfit } from "next/font/google"; import "./globals.css"; 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 = { title: "Pole Dance Championships", @@ -13,7 +25,7 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + {children} diff --git a/web/src/app/providers.tsx b/web/src/app/providers.tsx index 12877ec..02c5bad 100644 --- a/web/src/app/providers.tsx +++ b/web/src/app/providers.tsx @@ -14,8 +14,14 @@ function AuthInitializer({ children }: { children: React.ReactNode }) { if (!isInitialized) { return ( -
-
+
+
+
+
+
+
+

Loading

+
); } diff --git a/web/src/components/admin/UserCard.tsx b/web/src/components/admin/UserCard.tsx index 1ab8ae1..898fc58 100644 --- a/web/src/components/admin/UserCard.tsx +++ b/web/src/components/admin/UserCard.tsx @@ -2,6 +2,7 @@ import { UserOut } from "@/types/user"; import { Card, CardContent } from "@/components/ui/card"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; +import { Building2, Phone, AtSign, CheckCircle, XCircle } from "lucide-react"; interface Props { user: UserOut; @@ -11,47 +12,69 @@ interface Props { } const STATUS_DOT: Record = { - pending: "bg-orange-400", - approved: "bg-green-500", - rejected: "bg-red-500", + pending: "bg-gold-accent", + approved: "bg-emerald-500", + rejected: "bg-destructive", }; export function UserCard({ user, onApprove, onReject, isActing }: Props) { const initials = user.full_name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2); return ( - + - - + + {initials}
-

{user.full_name}

- +

{user.full_name}

+
-

{user.email}

- {user.organization_name &&

🏒 {user.organization_name}

} - {user.phone &&

πŸ“ž {user.phone}

} - {user.instagram_handle &&

πŸ“Έ {user.instagram_handle}

} +

{user.email}

+ {user.organization_name && ( +

+ + {user.organization_name} +

+ )} + {user.phone && ( +

+ + {user.phone} +

+ )} + {user.instagram_handle && ( +

+ + {user.instagram_handle} +

+ )}
{user.status === "pending" && onApprove && onReject && (
-
)} {user.status !== "pending" && ( - {user.status} + {user.status} )}
diff --git a/web/src/components/auth/MemberFields.tsx b/web/src/components/auth/MemberFields.tsx index 9288faa..dcab250 100644 --- a/web/src/components/auth/MemberFields.tsx +++ b/web/src/components/auth/MemberFields.tsx @@ -12,21 +12,21 @@ interface Props { export function MemberFields({ full_name, email, password, phone, onChange }: Props) { return ( <> -
- - onChange("full_name", e.target.value)} required /> +
+ + onChange("full_name", e.target.value)} required className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
-
- - onChange("email", e.target.value)} required /> +
+ + onChange("email", e.target.value)} required className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
-
- - onChange("password", e.target.value)} required /> +
+ + onChange("password", e.target.value)} required className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
-
- - onChange("phone", e.target.value)} /> +
+ + onChange("phone", e.target.value)} className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
); diff --git a/web/src/components/auth/OrganizerFields.tsx b/web/src/components/auth/OrganizerFields.tsx index dfd7204..aaea960 100644 --- a/web/src/components/auth/OrganizerFields.tsx +++ b/web/src/components/auth/OrganizerFields.tsx @@ -1,5 +1,6 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { AlertTriangle } from "lucide-react"; interface Props { organization_name: string; @@ -10,16 +11,17 @@ interface Props { export function OrganizerFields({ organization_name, instagram_handle, onChange }: Props) { return ( <> -

+

+ Organizer accounts require admin approval before you can log in. -

-
- - onChange("organization_name", e.target.value)} required />
-
- - onChange("instagram_handle", e.target.value)} /> +
+ + onChange("organization_name", e.target.value)} required className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" /> +
+
+ + onChange("instagram_handle", e.target.value)} className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30 placeholder:text-dim" />
); diff --git a/web/src/components/championships/ChampionshipCard.tsx b/web/src/components/championships/ChampionshipCard.tsx index 44779a6..4f10723 100644 --- a/web/src/components/championships/ChampionshipCard.tsx +++ b/web/src/components/championships/ChampionshipCard.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { Championship } from "@/types/championship"; import { StatusBadge } from "@/components/shared/StatusBadge"; import { Card, CardContent } from "@/components/ui/card"; +import { MapPin, Calendar, CreditCard } from "lucide-react"; interface Props { championship: Championship; @@ -14,22 +15,54 @@ export function ChampionshipCard({ championship: c }: Props) { return ( - - {c.image_url ? ( - {c.title} - ) : ( -
- πŸ† -
- )} - -
-

{c.title}

+ + {/* Image / gradient header */} +
+ {c.image_url ? ( + {c.title} + ) : ( +
+
+ πŸ’ƒ +
+
+ )} + {/* Gradient overlay */} +
+ {/* Status badge floating on image */} +
- {c.location &&

πŸ“ {c.location}

} - {date &&

πŸ“… {date}

} - {c.entry_fee != null &&

πŸ’³ {c.entry_fee} β‚½

} +
+ + +

+ {c.title} +

+
+ {c.location && ( +

+ + {c.location} +

+ )} + {date && ( +

+ + {date} +

+ )} + {c.entry_fee != null && ( +

+ + {c.entry_fee} β‚½ +

+ )} +
diff --git a/web/src/components/layout/Navbar.tsx b/web/src/components/layout/Navbar.tsx index 72015c2..72cd8d6 100644 --- a/web/src/components/layout/Navbar.tsx +++ b/web/src/components/layout/Navbar.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { useRouter } from "next/navigation"; +import { useRouter, usePathname } from "next/navigation"; import { useAuth } from "@/hooks/useAuth"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { @@ -11,16 +11,20 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { Trophy, ListChecks, Shield, Menu } from "lucide-react"; +import { useState } from "react"; const NAV_LINKS = [ - { href: "/championships", label: "Championships" }, - { href: "/registrations", label: "My Registrations" }, + { href: "/championships", label: "Championships", icon: Trophy }, + { href: "/registrations", label: "My Registrations", icon: ListChecks }, ]; export function Navbar() { const router = useRouter(); + const pathname = usePathname(); const user = useAuth((s) => s.user); const logout = useAuth((s) => s.logout); + const [mobileOpen, setMobileOpen] = useState(false); async function handleLogout() { await logout(); @@ -35,47 +39,115 @@ export function Navbar() { .slice(0, 2) ?? "?"; return ( -
+
- - πŸ† DanceChamp + + + DanceChamp + -
- - - - - - - Profile - - - - Sign out - - - +
+ {/* Mobile menu toggle */} + + + + + + + + + Profile + + + + Sign out + + + +
+ + {/* Mobile nav */} + {mobileOpen && ( + + )}
); } diff --git a/web/src/components/registrations/RegistrationCard.tsx b/web/src/components/registrations/RegistrationCard.tsx index 7f066f7..4470886 100644 --- a/web/src/components/registrations/RegistrationCard.tsx +++ b/web/src/components/registrations/RegistrationCard.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { Registration } from "@/types/registration"; import { StatusBadge } from "@/components/shared/StatusBadge"; 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"]; @@ -18,24 +19,38 @@ export function RegistrationCard({ registration: r }: Props) { return ( - +
-
-

{r.championship_title ?? "Championship"}

- {r.championship_location &&

πŸ“ {r.championship_location}

} - {date &&

πŸ“… {date}

} +
+

+ {r.championship_title ?? "Championship"} +

+ {r.championship_location && ( +

+ + {r.championship_location} +

+ )} + {date && ( +

+ + {date} +

+ )}
- {/* Progress dots */} + {/* Progress bar */}
{STEPS.map((_, i) => (
))} diff --git a/web/src/components/registrations/RegistrationTimeline.tsx b/web/src/components/registrations/RegistrationTimeline.tsx index 240d505..b7e60f8 100644 --- a/web/src/components/registrations/RegistrationTimeline.tsx +++ b/web/src/components/registrations/RegistrationTimeline.tsx @@ -1,4 +1,5 @@ import { Registration } from "@/types/registration"; +import { Check, AlertTriangle } from "lucide-react"; const STEPS: { key: string; label: string }[] = [ { key: "submitted", label: "Submitted" }, @@ -20,7 +21,8 @@ export function RegistrationTimeline({ registration }: Props) { if (isRejected) { return ( -
+
+ Your registration was rejected.
); @@ -28,26 +30,34 @@ export function RegistrationTimeline({ registration }: Props) { if (isWaitlisted) { return ( -
+
+ You are on the waitlist.
); } return ( -
-

Registration progress

-
    +
    +

    Registration progress

    +
      {STEPS.map((step, i) => { const done = i <= currentIndex; + const isCurrent = i === currentIndex; return ( -
    1. - - {done ? "βœ“" : i + 1} +
    2. + + {done ? : i + 1} + + + {step.label} - {step.label}
    3. ); })} diff --git a/web/src/components/shared/StatusBadge.tsx b/web/src/components/shared/StatusBadge.tsx index 61f3b61..d3ace3b 100644 --- a/web/src/components/shared/StatusBadge.tsx +++ b/web/src/components/shared/StatusBadge.tsx @@ -1,21 +1,21 @@ import { Badge } from "@/components/ui/badge"; const CHAMPIONSHIP_COLORS: Record = { - open: "bg-green-100 text-green-700", - closed: "bg-gray-100 text-gray-600", - draft: "bg-yellow-100 text-yellow-700", - completed: "bg-blue-100 text-blue-700", + open: "bg-emerald-500/15 text-emerald-400 border-emerald-500/20", + closed: "bg-surface-elevated text-dim border-border/40", + draft: "bg-gold-soft text-gold-accent border-gold-accent/20", + completed: "bg-blue-500/15 text-blue-400 border-blue-500/20", }; const REGISTRATION_COLORS: Record = { - submitted: "bg-gray-100 text-gray-600", - form_submitted: "bg-yellow-100 text-yellow-700", - payment_pending: "bg-orange-100 text-orange-700", - payment_confirmed: "bg-blue-100 text-blue-700", - video_submitted: "bg-violet-100 text-violet-700", - accepted: "bg-green-100 text-green-700", - rejected: "bg-red-100 text-red-700", - waitlisted: "bg-amber-100 text-amber-700", + submitted: "bg-surface-elevated text-muted-foreground border-border/40", + form_submitted: "bg-gold-soft text-gold-accent border-gold-accent/20", + payment_pending: "bg-orange-500/15 text-orange-400 border-orange-500/20", + payment_confirmed: "bg-blue-500/15 text-blue-400 border-blue-500/20", + video_submitted: "bg-purple-soft text-purple-accent border-purple-accent/20", + accepted: "bg-emerald-500/15 text-emerald-400 border-emerald-500/20", + rejected: "bg-destructive/15 text-destructive border-destructive/20", + waitlisted: "bg-gold-soft text-gold-accent border-gold-accent/20", }; interface Props { @@ -25,8 +25,10 @@ interface Props { export function StatusBadge({ status, type = "championship" }: Props) { 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 ( - {status.replace("_", " ")} + + {status.replace("_", " ")} + ); } diff --git a/web/src/styles/animations.css b/web/src/styles/animations.css new file mode 100644 index 0000000..9a35836 --- /dev/null +++ b/web/src/styles/animations.css @@ -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; } diff --git a/web/src/styles/theme.css b/web/src/styles/theme.css new file mode 100644 index 0000000..0bd6923 --- /dev/null +++ b/web/src/styles/theme.css @@ -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; +} diff --git a/web/src/styles/utilities.css b/web/src/styles/utilities.css new file mode 100644 index 0000000..97a7786 --- /dev/null +++ b/web/src/styles/utilities.css @@ -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; +}