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:
@@ -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 <div className="flex justify-center py-20 text-gray-400">Loading…</div>;
|
||||
if (error) return <p className="text-center text-red-500 py-20">Failed to load users.</p>;
|
||||
if (isLoading) {
|
||||
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 shown = filter === "pending" ? pending : (data ?? []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="mb-6 text-2xl font-bold text-gray-900">User Management</h1>
|
||||
<div className="animate-fade-in">
|
||||
<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">
|
||||
<button
|
||||
onClick={() => setFilter("pending")}
|
||||
className={`rounded-full px-4 py-1.5 text-sm font-medium transition-colors ${
|
||||
filter === "pending" ? "bg-violet-600 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
Pending ({pending.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter("all")}
|
||||
className={`rounded-full px-4 py-1.5 text-sm font-medium transition-colors ${
|
||||
filter === "all" ? "bg-violet-600 text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
All users ({data?.length ?? 0})
|
||||
</button>
|
||||
<div className="mb-5 flex gap-2">
|
||||
{(["pending", "all"] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`rounded-full px-4 py-1.5 text-sm font-medium transition-all duration-200 ${
|
||||
filter === f
|
||||
? "bg-rose-accent text-white glow-rose"
|
||||
: "bg-surface-elevated border border-border/40 text-muted-foreground hover:text-foreground hover:border-border"
|
||||
}`}
|
||||
>
|
||||
{f === "pending" ? `Pending (${pending.length})` : `All users (${data?.length ?? 0})`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{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">
|
||||
{shown.map((u) => (
|
||||
<UserCard
|
||||
key={u.id}
|
||||
user={u}
|
||||
onApprove={(id) => approve.mutate(id)}
|
||||
onReject={(id) => reject.mutate(id)}
|
||||
isActing={approve.isPending || reject.isPending}
|
||||
/>
|
||||
{shown.map((u, i) => (
|
||||
<div key={u.id} className={`animate-fade-in-up stagger-${Math.min(i + 1, 9)}`}>
|
||||
<UserCard
|
||||
user={u}
|
||||
onApprove={(id) => approve.mutate(id)}
|
||||
onReject={(id) => reject.mutate(id)}
|
||||
isActing={approve.isPending || reject.isPending}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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 <div className="flex justify-center py-20 text-gray-400">Loading…</div>;
|
||||
if (error || !championship) return <p className="text-center text-red-500 py-20">Championship not found.</p>;
|
||||
if (isLoading) {
|
||||
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 canRegister = championship.status === "open" && !myReg;
|
||||
@@ -29,51 +37,77 @@ export default function ChampionshipDetailPage({ params }: { params: Promise<{ i
|
||||
: null;
|
||||
|
||||
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 */}
|
||||
{championship.image_url ? (
|
||||
<img src={championship.image_url} alt={championship.title} className="w-full h-56 object-cover rounded-xl" />
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
<div className="relative overflow-hidden rounded-2xl">
|
||||
{championship.image_url ? (
|
||||
<img src={championship.image_url} alt={championship.title} className="w-full h-56 object-cover" />
|
||||
) : (
|
||||
<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">
|
||||
<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 */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">{championship.title}</h1>
|
||||
{championship.subtitle && <p className="text-gray-500 mt-1">{championship.subtitle}</p>}
|
||||
<h1 className="font-display text-3xl font-bold tracking-wide">{championship.title}</h1>
|
||||
{championship.subtitle && <p className="text-muted-foreground mt-1">{championship.subtitle}</p>}
|
||||
</div>
|
||||
<StatusBadge status={championship.status} />
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div className="space-y-2 text-sm text-gray-600">
|
||||
{championship.location && <p>📍 {championship.location}</p>}
|
||||
{championship.venue && <p>🏛 {championship.venue}</p>}
|
||||
{eventDate && <p>📅 {eventDate}</p>}
|
||||
{championship.entry_fee != null && <p>💳 Entry fee: <strong>{championship.entry_fee} ₽</strong></p>}
|
||||
{/* Details grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{championship.location && (
|
||||
<div className="flex items-center gap-2.5 rounded-xl bg-surface-elevated border border-border/30 px-4 py-3 text-sm">
|
||||
<MapPin size={16} className="text-rose-accent shrink-0" />
|
||||
<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 && (
|
||||
<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>
|
||||
|
||||
{championship.description && (
|
||||
<>
|
||||
<Separator />
|
||||
<p className="text-gray-700 whitespace-pre-line">{championship.description}</p>
|
||||
<Separator className="bg-border/30" />
|
||||
<p className="text-muted-foreground whitespace-pre-line leading-relaxed">{championship.description}</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
<Separator className="bg-border/30" />
|
||||
|
||||
{/* Registration section */}
|
||||
{myReg && <RegistrationTimeline registration={myReg} />}
|
||||
|
||||
{canRegister && (
|
||||
<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}
|
||||
onClick={() => registerMutation.mutate()}
|
||||
>
|
||||
@@ -82,7 +116,7 @@ export default function ChampionshipDetailPage({ params }: { params: Promise<{ i
|
||||
)}
|
||||
|
||||
{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 && (
|
||||
@@ -90,9 +124,10 @@ export default function ChampionshipDetailPage({ params }: { params: Promise<{ i
|
||||
href={championship.form_url}
|
||||
target="_blank"
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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 <div className="flex justify-center py-20 text-gray-400">Loading…</div>;
|
||||
if (error) return <p className="text-center text-red-500 py-20">Failed to load championships.</p>;
|
||||
if (!data?.length) return <p className="text-center text-gray-400 py-20">No championships yet.</p>;
|
||||
if (isLoading) {
|
||||
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 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 (
|
||||
<div>
|
||||
<h1 className="mb-6 text-2xl font-bold text-gray-900">Championships</h1>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{data.map((c) => (
|
||||
<ChampionshipCard key={c.id} championship={c} />
|
||||
<div className="animate-fade-in">
|
||||
<div className="mb-8">
|
||||
<h1 className="font-display text-4xl font-bold tracking-wide">Championships</h1>
|
||||
<p className="mt-1 text-muted-foreground">Browse upcoming competitions</p>
|
||||
</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>
|
||||
|
||||
@@ -2,9 +2,9 @@ import { Navbar } from "@/components/layout/Navbar";
|
||||
|
||||
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen bg-mesh">
|
||||
<Navbar />
|
||||
<main className="mx-auto max-w-6xl px-4 py-8">{children}</main>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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 (
|
||||
<div className="max-w-md mx-auto space-y-6">
|
||||
<div className="flex flex-col items-center gap-3 pt-4">
|
||||
<Avatar className="h-20 w-20">
|
||||
<AvatarFallback className="bg-violet-100 text-violet-700 text-2xl font-bold">
|
||||
{initials}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-bold text-gray-900">{user.full_name}</p>
|
||||
<p className="text-sm text-gray-500">{user.email}</p>
|
||||
<div className="max-w-md mx-auto space-y-6 animate-fade-in">
|
||||
<div className="flex flex-col items-center gap-4 pt-4">
|
||||
{/* Avatar with gradient ring */}
|
||||
<div className="relative">
|
||||
<div className="absolute -inset-1 rounded-full bg-gradient-to-br from-rose-accent via-purple-accent to-gold-accent opacity-50 blur-sm" />
|
||||
<Avatar className="relative h-20 w-20 border-2 border-background">
|
||||
<AvatarFallback className="bg-surface-elevated text-rose-accent text-2xl font-bold">
|
||||
{initials}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</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}
|
||||
</Badge>
|
||||
</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 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Phone</span>
|
||||
<span>{user.phone}</span>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="flex items-center gap-2 text-sm text-dim">
|
||||
<Phone size={14} />
|
||||
Phone
|
||||
</span>
|
||||
<span className="text-sm text-foreground">{user.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
{user.organization_name && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Organization</span>
|
||||
<span>{user.organization_name}</span>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="flex items-center gap-2 text-sm text-dim">
|
||||
<Building2 size={14} />
|
||||
Organization
|
||||
</span>
|
||||
<span className="text-sm text-foreground">{user.organization_name}</span>
|
||||
</div>
|
||||
)}
|
||||
{user.instagram_handle && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Instagram</span>
|
||||
<span>{user.instagram_handle}</span>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="flex items-center gap-2 text-sm text-dim">
|
||||
<AtSign size={14} />
|
||||
AtSign
|
||||
</span>
|
||||
<span className="text-sm text-foreground">{user.instagram_handle}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Member since</span>
|
||||
<span>{joinedDate}</span>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="flex items-center gap-2 text-sm text-dim">
|
||||
<CalendarDays size={14} />
|
||||
Member since
|
||||
</span>
|
||||
<span className="text-sm text-foreground">{joinedDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Button variant="destructive" className="w-full" onClick={handleLogout}>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full border-destructive/30 text-destructive hover:bg-destructive/10 hover:border-destructive/50"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<LogOut size={16} />
|
||||
Sign out
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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 <div className="flex justify-center py-20 text-gray-400">Loading…</div>;
|
||||
if (error) return <p className="text-center text-red-500 py-20">Failed to load registrations.</p>;
|
||||
if (!data?.length) return (
|
||||
<div className="text-center py-20 text-gray-400">
|
||||
<p className="text-4xl mb-3">📋</p>
|
||||
<p>No registrations yet.</p>
|
||||
</div>
|
||||
);
|
||||
if (isLoading) {
|
||||
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 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 (
|
||||
<div>
|
||||
<h1 className="mb-6 text-2xl font-bold text-gray-900">My Registrations</h1>
|
||||
<div className="animate-fade-in">
|
||||
<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">
|
||||
{data.map((r) => (
|
||||
<RegistrationCard key={r.id} registration={r} />
|
||||
{data.map((r, i) => (
|
||||
<div key={r.id} className={`animate-fade-in-up stagger-${Math.min(i + 1, 9)}`}>
|
||||
<RegistrationCard registration={r} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,38 @@
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
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="w-full max-w-md">{children}</div>
|
||||
<div className="relative flex min-h-screen items-center justify-center overflow-hidden p-4">
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-2 text-4xl">🏆</div>
|
||||
<CardTitle className="text-2xl">Welcome back</CardTitle>
|
||||
<CardDescription>Sign in to your account</CardDescription>
|
||||
<Card className="glass-strong glow-rose overflow-hidden">
|
||||
<CardHeader className="text-center pb-2">
|
||||
<div className="mx-auto mb-4 h-px w-16 bg-gradient-to-r from-transparent via-rose-accent to-transparent" />
|
||||
<CardTitle className="font-display text-3xl font-semibold tracking-wide">
|
||||
Welcome back
|
||||
</CardTitle>
|
||||
<CardDescription className="text-muted-foreground">
|
||||
Sign in to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<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">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-xs uppercase tracking-widest text-dim">
|
||||
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 className="space-y-1">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="text-xs uppercase tracking-widest text-dim">
|
||||
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>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex flex-col gap-3">
|
||||
<Button type="submit" className="w-full bg-violet-600 hover:bg-violet-700" disabled={isLoading}>
|
||||
<CardFooter className="flex flex-col gap-4 pt-2">
|
||||
<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"}
|
||||
</Button>
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
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
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -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 (
|
||||
<Card className="text-center">
|
||||
<Card className="glass-strong glow-purple text-center overflow-hidden">
|
||||
<CardHeader>
|
||||
<div className="mx-auto mb-2 text-5xl">⏳</div>
|
||||
<CardTitle className="text-2xl">Awaiting approval</CardTitle>
|
||||
<CardDescription>
|
||||
<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">
|
||||
<Clock className="h-7 w-7 text-purple-accent" />
|
||||
</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.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<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.
|
||||
</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>
|
||||
</Button>
|
||||
</CardContent>
|
||||
|
||||
@@ -11,16 +11,24 @@ export default function RegisterPage() {
|
||||
const { role, setRole, form, update, error, isLoading, submit } = useRegisterForm();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-2 text-4xl">🏅</div>
|
||||
<CardTitle className="text-2xl">Create account</CardTitle>
|
||||
<CardDescription>Join the pole dance community</CardDescription>
|
||||
<Card className="glass-strong glow-rose overflow-hidden">
|
||||
<CardHeader className="text-center pb-2">
|
||||
<div className="mx-auto mb-4 h-px w-16 bg-gradient-to-r from-transparent via-purple-accent to-transparent" />
|
||||
<CardTitle className="font-display text-3xl font-semibold tracking-wide">
|
||||
Create account
|
||||
</CardTitle>
|
||||
<CardDescription className="text-muted-foreground">
|
||||
Join the pole dance community
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<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">
|
||||
{(["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"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -54,13 +64,17 @@ export default function RegisterPage() {
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex flex-col gap-3">
|
||||
<Button type="submit" className="w-full bg-violet-600 hover:bg-violet-700" disabled={isLoading}>
|
||||
<CardFooter className="flex flex-col gap-4 pt-2">
|
||||
<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"}
|
||||
</Button>
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
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
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -14,8 +14,14 @@ function AuthInitializer({ children }: { children: React.ReactNode }) {
|
||||
|
||||
if (!isInitialized) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-violet-600 border-t-transparent" />
|
||||
<div className="flex h-screen items-center justify-center bg-background">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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 (
|
||||
<Card>
|
||||
<Card className="border-border/40 bg-surface-elevated">
|
||||
<CardContent className="p-4 flex gap-4 items-start">
|
||||
<Avatar className="h-10 w-10 shrink-0">
|
||||
<AvatarFallback className="bg-violet-100 text-violet-700 text-sm font-semibold">
|
||||
<Avatar className="h-10 w-10 shrink-0 border border-border/40">
|
||||
<AvatarFallback className="bg-rose-accent/10 text-rose-accent text-sm font-semibold">
|
||||
{initials}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="flex-1 min-w-0 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-semibold text-gray-900">{user.full_name}</p>
|
||||
<span className={`h-2 w-2 rounded-full shrink-0 ${STATUS_DOT[user.status] ?? "bg-gray-400"}`} />
|
||||
<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-dim"}`} />
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{user.email}</p>
|
||||
{user.organization_name && <p className="text-sm text-gray-500">🏢 {user.organization_name}</p>}
|
||||
{user.phone && <p className="text-sm text-gray-500">📞 {user.phone}</p>}
|
||||
{user.instagram_handle && <p className="text-sm text-gray-500">📸 {user.instagram_handle}</p>}
|
||||
<p className="text-sm text-muted-foreground">{user.email}</p>
|
||||
{user.organization_name && (
|
||||
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<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>
|
||||
|
||||
{user.status === "pending" && onApprove && onReject && (
|
||||
<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
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive" disabled={isActing} onClick={() => onReject(user.id)}>
|
||||
<XCircle size={14} />
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
</Card>
|
||||
|
||||
@@ -12,21 +12,21 @@ interface Props {
|
||||
export function MemberFields({ full_name, email, password, phone, onChange }: Props) {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="full_name">Full name</Label>
|
||||
<Input id="full_name" value={full_name} onChange={(e) => onChange("full_name", e.target.value)} required />
|
||||
<div className="space-y-2">
|
||||
<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 className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" value={email} onChange={(e) => onChange("email", e.target.value)} required />
|
||||
<div className="space-y-2">
|
||||
<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 className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" value={password} onChange={(e) => onChange("password", e.target.value)} required />
|
||||
<div className="space-y-2">
|
||||
<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 className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="phone">Phone (optional)</Label>
|
||||
<Input id="phone" type="tel" value={phone} onChange={(e) => onChange("phone", e.target.value)} />
|
||||
<div className="space-y-2">
|
||||
<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)} className="bg-surface border-border/60 focus:border-rose-accent focus:ring-rose-accent/30" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<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.
|
||||
</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 className="space-y-1">
|
||||
<Label htmlFor="ig">Instagram (optional)</Label>
|
||||
<Input id="ig" placeholder="@yourstudio" value={instagram_handle} onChange={(e) => onChange("instagram_handle", e.target.value)} />
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="org" className="text-xs uppercase tracking-widest text-dim">Organization name</Label>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Link href={`/championships/${c.id}`}>
|
||||
<Card className="overflow-hidden transition-shadow hover:shadow-md cursor-pointer h-full">
|
||||
{c.image_url ? (
|
||||
<img src={c.image_url} alt={c.title} className="h-40 w-full object-cover" />
|
||||
) : (
|
||||
<div className="h-40 w-full bg-gradient-to-br from-violet-400 to-purple-600 flex items-center justify-center text-4xl">
|
||||
🏆
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
<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">
|
||||
{/* Image / gradient header */}
|
||||
<div className="relative overflow-hidden">
|
||||
{c.image_url ? (
|
||||
<img
|
||||
src={c.image_url}
|
||||
alt={c.title}
|
||||
className="h-44 w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<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} />
|
||||
</div>
|
||||
{c.location && <p className="text-sm text-gray-500">📍 {c.location}</p>}
|
||||
{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>}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
@@ -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 (
|
||||
<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="flex items-center gap-6">
|
||||
<Link href="/championships" className="text-lg font-bold text-violet-700">
|
||||
🏆 DanceChamp
|
||||
<Link href="/championships" className="flex items-center gap-2 group">
|
||||
<span className="text-gradient-rose font-display text-xl font-bold tracking-wide">
|
||||
DanceChamp
|
||||
</span>
|
||||
</Link>
|
||||
<nav className="hidden gap-4 text-sm font-medium text-gray-600 sm:flex">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<Link key={link.href} href={link.href} className="hover:text-violet-700 transition-colors">
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* Desktop nav */}
|
||||
<nav className="hidden gap-1 sm:flex">
|
||||
{NAV_LINKS.map((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" && (
|
||||
<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
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="rounded-full focus:outline-none focus:ring-2 focus:ring-violet-500">
|
||||
<Avatar className="h-8 w-8 cursor-pointer">
|
||||
<AvatarFallback className="bg-violet-100 text-violet-700 text-xs font-semibold">
|
||||
{initials}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-44">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/profile">Profile</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleLogout} className="text-red-600 focus:text-red-600">
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Mobile menu toggle */}
|
||||
<button
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
className="rounded-lg p-1.5 text-muted-foreground hover:text-foreground hover:bg-surface-hover transition-colors sm:hidden"
|
||||
>
|
||||
<Menu size={20} />
|
||||
</button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="rounded-full focus:outline-none focus:ring-2 focus:ring-rose-accent/50">
|
||||
<Avatar className="h-8 w-8 cursor-pointer border border-rose-accent/30">
|
||||
<AvatarFallback className="bg-rose-accent/10 text-rose-accent text-xs font-semibold">
|
||||
{initials}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<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">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">{r.championship_title ?? "Championship"}</p>
|
||||
{r.championship_location && <p className="text-sm text-gray-500">📍 {r.championship_location}</p>}
|
||||
{date && <p className="text-sm text-gray-500">📅 {date}</p>}
|
||||
<div className="space-y-1.5">
|
||||
<p className="font-semibold text-foreground group-hover:text-rose-accent transition-colors">
|
||||
{r.championship_title ?? "Championship"}
|
||||
</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>
|
||||
<StatusBadge status={r.status} type="registration" />
|
||||
</div>
|
||||
|
||||
{/* Progress dots */}
|
||||
{/* Progress bar */}
|
||||
<div className="flex gap-1.5">
|
||||
{STEPS.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-2 flex-1 rounded-full ${
|
||||
i <= stepIndex ? "bg-violet-500" : "bg-gray-200"
|
||||
className={`h-1.5 flex-1 rounded-full transition-colors ${
|
||||
i <= stepIndex
|
||||
? "bg-gradient-to-r from-rose-accent to-purple-accent"
|
||||
: "bg-border/40"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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 (
|
||||
<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>.
|
||||
</div>
|
||||
);
|
||||
@@ -28,26 +30,34 @@ export function RegistrationTimeline({ registration }: Props) {
|
||||
|
||||
if (isWaitlisted) {
|
||||
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>.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-gray-700">Registration progress</p>
|
||||
<ol className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-medium text-foreground">Registration progress</p>
|
||||
<ol className="space-y-1">
|
||||
{STEPS.map((step, i) => {
|
||||
const done = i <= currentIndex;
|
||||
const isCurrent = i === currentIndex;
|
||||
return (
|
||||
<li key={step.key} className="flex items-center gap-3 text-sm">
|
||||
<span className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-xs font-bold ${
|
||||
done ? "bg-violet-600 text-white" : "bg-gray-200 text-gray-400"
|
||||
}`}>
|
||||
{done ? "✓" : i + 1}
|
||||
<li key={step.key} className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm">
|
||||
<span className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-bold transition-all ${
|
||||
done
|
||||
? "bg-rose-accent text-white"
|
||||
: "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 className={done ? "text-gray-900" : "text-gray-400"}>{step.label}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
const CHAMPIONSHIP_COLORS: Record<string, string> = {
|
||||
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<string, string> = {
|
||||
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 (
|
||||
<Badge className={`${color} border-0 capitalize`}>{status.replace("_", " ")}</Badge>
|
||||
<Badge className={`${color} border capitalize text-[11px] tracking-wide`}>
|
||||
{status.replace("_", " ")}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
62
web/src/styles/animations.css
Normal file
62
web/src/styles/animations.css
Normal 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
38
web/src/styles/theme.css
Normal 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;
|
||||
}
|
||||
108
web/src/styles/utilities.css
Normal file
108
web/src/styles/utilities.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user