feat: Implement a cyberpunk-themed UI with new decorative components, illustrations, and visual effects.
This commit is contained in:
parent
c49e56b1e7
commit
059e6eedf9
@ -7,7 +7,10 @@ import { ContestCard } from "@/components/domain/contest-card";
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { AlertError } from "@/components/ui/alert";
|
import { AlertError } from "@/components/ui/alert";
|
||||||
import { Calendar, Clock, History } from "lucide-react";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Calendar, Clock, History, Trophy, Zap, Activity } from "lucide-react";
|
||||||
|
import { GlowOrbs, GlitchText } from "@/components/decorative";
|
||||||
|
import { EmptyStateIllustration } from "@/components/illustrations";
|
||||||
import type { ContestListItem } from "@/types";
|
import type { ContestListItem } from "@/types";
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
@ -59,10 +62,13 @@ export default function ContestsPage() {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<Skeleton className="h-10 w-48 mb-8" />
|
<div className="flex items-center gap-4 mb-8">
|
||||||
<div className="space-y-4">
|
<Skeleton className="h-10 w-10 rounded-lg" />
|
||||||
{[...Array(3)].map((_, i) => (
|
<Skeleton className="h-10 w-48" />
|
||||||
<Skeleton key={i} className="h-32 w-full rounded-xl" />
|
</div>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{[...Array(6)].map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-48 w-full rounded-xl" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -78,68 +84,136 @@ export default function ContestsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="min-h-[calc(100vh-4rem)] relative">
|
||||||
<div className="flex items-center justify-between mb-8">
|
{/* Background */}
|
||||||
<h1 className="text-3xl font-bold">Контесты</h1>
|
<GlowOrbs />
|
||||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
<div className="absolute inset-0 cyber-grid opacity-10" />
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<div className="w-2 h-2 rounded-full bg-success animate-pulse" />
|
|
||||||
{activeContests.length} активных
|
|
||||||
</span>
|
|
||||||
<span>{contests.length} всего</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
|
<div className="container mx-auto px-4 py-8 relative">
|
||||||
<TabsList>
|
{/* Header */}
|
||||||
<TabsTrigger value="all" className="gap-2">
|
|
||||||
<Calendar className="h-4 w-4" />
|
|
||||||
Все ({contests.length})
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="active" className="gap-2">
|
|
||||||
<Clock className="h-4 w-4" />
|
|
||||||
Активные ({activeContests.length})
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="upcoming" className="gap-2">
|
|
||||||
<Calendar className="h-4 w-4" />
|
|
||||||
Предстоящие ({upcomingContests.length})
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="past" className="gap-2">
|
|
||||||
<History className="h-4 w-4" />
|
|
||||||
Прошедшие ({pastContests.length})
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
{filteredContests().length > 0 ? (
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"
|
initial={{ opacity: 0, y: -20 }}
|
||||||
variants={containerVariants}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
initial="hidden"
|
className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-8"
|
||||||
animate="visible"
|
|
||||||
>
|
>
|
||||||
{filteredContests().map((contest) => (
|
<div className="flex items-center gap-4">
|
||||||
<motion.div key={contest.id} variants={itemVariants}>
|
<div className="w-12 h-12 rounded-lg bg-[var(--color-neon-green)]/10 border border-[var(--color-neon-green)]/30 flex items-center justify-center">
|
||||||
<ContestCard
|
<Trophy className="h-6 w-6 text-[var(--color-neon-green)]" />
|
||||||
contest={{
|
</div>
|
||||||
id: contest.id,
|
<div>
|
||||||
title: contest.title,
|
<h1 className="text-3xl font-display font-bold">
|
||||||
start_time: contest.start_time,
|
<GlitchText text="КОНТЕСТЫ" intensity="low" />
|
||||||
end_time: contest.end_time,
|
</h1>
|
||||||
is_active: contest.is_active,
|
<p className="text-sm font-mono text-muted-foreground">
|
||||||
problems_count: contest.problems_count,
|
<span className="text-[var(--color-neon-cyan)]">$</span> active_competitions.list()
|
||||||
participants_count: contest.participants_count,
|
</p>
|
||||||
}}
|
</div>
|
||||||
/>
|
</div>
|
||||||
</motion.div>
|
|
||||||
))}
|
{/* Stats */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{activeContests.length > 0 && (
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[var(--color-neon-green)]/10 border border-[var(--color-neon-green)]/30">
|
||||||
|
<Activity className="h-4 w-4 text-[var(--color-neon-green)] animate-pulse" />
|
||||||
|
<span className="text-sm font-mono text-[var(--color-neon-green)]">
|
||||||
|
{activeContests.length} LIVE
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-sm font-mono text-muted-foreground">
|
||||||
|
<span className="text-[var(--color-neon-cyan)]">{contests.length}</span> всего
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
|
||||||
<div className="text-center text-muted-foreground py-16">
|
{/* Tabs */}
|
||||||
<Calendar className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-8">
|
||||||
<p className="text-lg">Контестов в этой категории пока нет</p>
|
<TabsList className="bg-[var(--color-card)] border border-[var(--color-border)] p-1">
|
||||||
</div>
|
<TabsTrigger
|
||||||
)}
|
value="all"
|
||||||
|
className="gap-2 font-mono data-[state=active]:bg-[var(--color-neon-green)]/10 data-[state=active]:text-[var(--color-neon-green)] data-[state=active]:border-[var(--color-neon-green)]/30"
|
||||||
|
>
|
||||||
|
<Calendar className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">Все</span>
|
||||||
|
<Badge variant="outline" className="ml-1 font-mono text-xs">
|
||||||
|
{contests.length}
|
||||||
|
</Badge>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="active"
|
||||||
|
className="gap-2 font-mono data-[state=active]:bg-[var(--color-neon-green)]/10 data-[state=active]:text-[var(--color-neon-green)] data-[state=active]:border-[var(--color-neon-green)]/30"
|
||||||
|
>
|
||||||
|
<Zap className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">Активные</span>
|
||||||
|
{activeContests.length > 0 && (
|
||||||
|
<Badge variant="success" pulse className="ml-1">
|
||||||
|
{activeContests.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="upcoming"
|
||||||
|
className="gap-2 font-mono data-[state=active]:bg-[var(--color-neon-cyan)]/10 data-[state=active]:text-[var(--color-neon-cyan)] data-[state=active]:border-[var(--color-neon-cyan)]/30"
|
||||||
|
>
|
||||||
|
<Clock className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">Предстоящие</span>
|
||||||
|
<Badge variant="cyan" className="ml-1 font-mono text-xs">
|
||||||
|
{upcomingContests.length}
|
||||||
|
</Badge>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="past"
|
||||||
|
className="gap-2 font-mono data-[state=active]:bg-[var(--color-neon-purple)]/10 data-[state=active]:text-[var(--color-neon-purple)] data-[state=active]:border-[var(--color-neon-purple)]/30"
|
||||||
|
>
|
||||||
|
<History className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">Прошедшие</span>
|
||||||
|
<Badge variant="purple" className="ml-1 font-mono text-xs">
|
||||||
|
{pastContests.length}
|
||||||
|
</Badge>
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* Contest Grid */}
|
||||||
|
{filteredContests().length > 0 ? (
|
||||||
|
<motion.div
|
||||||
|
className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
>
|
||||||
|
{filteredContests().map((contest) => (
|
||||||
|
<motion.div key={contest.id} variants={itemVariants}>
|
||||||
|
<ContestCard
|
||||||
|
contest={{
|
||||||
|
id: contest.id,
|
||||||
|
title: contest.title,
|
||||||
|
start_time: contest.start_time,
|
||||||
|
end_time: contest.end_time,
|
||||||
|
is_active: contest.is_active,
|
||||||
|
problems_count: contest.problems_count,
|
||||||
|
participants_count: contest.participants_count,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="flex flex-col items-center justify-center py-16"
|
||||||
|
>
|
||||||
|
<EmptyStateIllustration type="no-contests" />
|
||||||
|
<p className="text-lg font-mono text-muted-foreground mt-4">
|
||||||
|
<span className="text-[var(--color-neon-cyan)]">></span> Контестов в этой категории пока нет
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-mono text-muted-foreground/60 mt-2">
|
||||||
|
// check back later
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,84 +1,237 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* Base colors */
|
/* ========================================
|
||||||
--color-background: #ffffff;
|
CYBERPUNK THEME - ВолГУ.Контесты
|
||||||
--color-foreground: #0a0a0a;
|
======================================== */
|
||||||
--color-primary: #2563eb;
|
|
||||||
--color-primary-foreground: #ffffff;
|
/* Base colors - Dark theme by default */
|
||||||
--color-secondary: #f1f5f9;
|
--color-background: #0a0a0f;
|
||||||
--color-secondary-foreground: #0f172a;
|
--color-foreground: #e4e4e7;
|
||||||
--color-muted: #f1f5f9;
|
|
||||||
--color-muted-foreground: #64748b;
|
/* Neon accent colors */
|
||||||
--color-accent: #f1f5f9;
|
--color-neon-green: #00ff88;
|
||||||
--color-accent-foreground: #0f172a;
|
--color-neon-cyan: #00f5ff;
|
||||||
--color-destructive: #ef4444;
|
--color-neon-purple: #bf00ff;
|
||||||
|
--color-neon-pink: #ff0080;
|
||||||
|
--color-neon-yellow: #f0ff00;
|
||||||
|
--color-neon-orange: #ff6b00;
|
||||||
|
|
||||||
|
/* Semantic color mapping */
|
||||||
|
--color-primary: #00ff88;
|
||||||
|
--color-primary-foreground: #0a0a0f;
|
||||||
|
--color-secondary: #1a1a2e;
|
||||||
|
--color-secondary-foreground: #e4e4e7;
|
||||||
|
--color-muted: #1a1a2e;
|
||||||
|
--color-muted-foreground: #71717a;
|
||||||
|
--color-accent: #1e1e3f;
|
||||||
|
--color-accent-foreground: #00f5ff;
|
||||||
|
--color-destructive: #ff3366;
|
||||||
--color-destructive-foreground: #ffffff;
|
--color-destructive-foreground: #ffffff;
|
||||||
--color-popover: #ffffff;
|
--color-popover: #0f0f1a;
|
||||||
--color-popover-foreground: #0a0a0a;
|
--color-popover-foreground: #e4e4e7;
|
||||||
--color-card: #ffffff;
|
--color-card: #0f0f1a;
|
||||||
--color-card-foreground: #0a0a0a;
|
--color-card-foreground: #e4e4e7;
|
||||||
--color-border: #e2e8f0;
|
--color-border: #2a2a4a;
|
||||||
--color-input: #e2e8f0;
|
--color-input: #1a1a2e;
|
||||||
--color-ring: #2563eb;
|
--color-ring: #00ff88;
|
||||||
--radius: 0.5rem;
|
--radius: 0.25rem;
|
||||||
|
|
||||||
/* Additional semantic colors */
|
/* Semantic status colors */
|
||||||
--color-success: #22c55e;
|
--color-success: #00ff88;
|
||||||
--color-success-foreground: #ffffff;
|
--color-success-foreground: #0a0a0f;
|
||||||
--color-warning: #f59e0b;
|
--color-warning: #f0ff00;
|
||||||
--color-warning-foreground: #000000;
|
--color-warning-foreground: #0a0a0f;
|
||||||
--color-info: #3b82f6;
|
--color-info: #00f5ff;
|
||||||
--color-info-foreground: #ffffff;
|
--color-info-foreground: #0a0a0f;
|
||||||
|
|
||||||
/* Card shadow */
|
/* Glow effects */
|
||||||
--shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
--glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
|
||||||
--shadow-card-hover: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
--glow-green-intense: 0 0 30px rgba(0, 255, 136, 0.7), 0 0 60px rgba(0, 255, 136, 0.3);
|
||||||
|
--glow-cyan: 0 0 20px rgba(0, 245, 255, 0.5);
|
||||||
|
--glow-cyan-intense: 0 0 30px rgba(0, 245, 255, 0.7), 0 0 60px rgba(0, 245, 255, 0.3);
|
||||||
|
--glow-purple: 0 0 20px rgba(191, 0, 255, 0.5);
|
||||||
|
--glow-purple-intense: 0 0 30px rgba(191, 0, 255, 0.7), 0 0 60px rgba(191, 0, 255, 0.3);
|
||||||
|
--glow-pink: 0 0 20px rgba(255, 0, 128, 0.5);
|
||||||
|
--glow-yellow: 0 0 20px rgba(240, 255, 0, 0.5);
|
||||||
|
--glow-orange: 0 0 20px rgba(255, 107, 0, 0.5);
|
||||||
|
|
||||||
|
/* Card shadows */
|
||||||
|
--shadow-card: 0 4px 20px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||||
|
--shadow-card-hover: 0 8px 30px rgba(0, 0, 0, 0.6), 0 0 20px rgba(0, 255, 136, 0.1);
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-display: 'Orbitron', sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
/* ========================================
|
||||||
@theme {
|
BASE STYLES
|
||||||
--color-background: #0a0a0a;
|
======================================== */
|
||||||
--color-foreground: #fafafa;
|
|
||||||
--color-primary: #3b82f6;
|
|
||||||
--color-primary-foreground: #ffffff;
|
|
||||||
--color-secondary: #1e293b;
|
|
||||||
--color-secondary-foreground: #f8fafc;
|
|
||||||
--color-muted: #1e293b;
|
|
||||||
--color-muted-foreground: #94a3b8;
|
|
||||||
--color-accent: #1e293b;
|
|
||||||
--color-accent-foreground: #f8fafc;
|
|
||||||
--color-destructive: #dc2626;
|
|
||||||
--color-destructive-foreground: #ffffff;
|
|
||||||
--color-popover: #1c1c1c;
|
|
||||||
--color-popover-foreground: #fafafa;
|
|
||||||
--color-card: #1c1c1c;
|
|
||||||
--color-card-foreground: #fafafa;
|
|
||||||
--color-border: #334155;
|
|
||||||
--color-input: #334155;
|
|
||||||
--color-ring: #3b82f6;
|
|
||||||
|
|
||||||
/* Dark mode semantic colors */
|
|
||||||
--color-success: #4ade80;
|
|
||||||
--color-success-foreground: #000000;
|
|
||||||
--color-warning: #fbbf24;
|
|
||||||
--color-warning-foreground: #000000;
|
|
||||||
--color-info: #60a5fa;
|
|
||||||
--color-info-foreground: #000000;
|
|
||||||
|
|
||||||
/* Dark mode shadows */
|
|
||||||
--shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
|
|
||||||
--shadow-card-hover: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
color: var(--color-foreground);
|
color: var(--color-foreground);
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: var(--font-mono), system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* ========================================
|
||||||
|
CYBERPUNK ANIMATIONS
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* Glitch effect */
|
||||||
|
@keyframes glitch {
|
||||||
|
0%, 100% {
|
||||||
|
clip-path: inset(0 0 0 0);
|
||||||
|
transform: translate(0);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
clip-path: inset(20% 0 60% 0);
|
||||||
|
transform: translate(-2px, 2px);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
clip-path: inset(40% 0 40% 0);
|
||||||
|
transform: translate(2px, -2px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
clip-path: inset(60% 0 20% 0);
|
||||||
|
transform: translate(-1px, 1px);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
clip-path: inset(80% 0 5% 0);
|
||||||
|
transform: translate(1px, -1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glitch-text {
|
||||||
|
0%, 100% {
|
||||||
|
text-shadow: 2px 0 var(--color-neon-cyan), -2px 0 var(--color-neon-pink);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
text-shadow: -2px 0 var(--color-neon-cyan), 2px 0 var(--color-neon-pink);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
text-shadow: 2px 0 var(--color-neon-pink), -2px 0 var(--color-neon-cyan);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
text-shadow: -2px 0 var(--color-neon-pink), 2px 0 var(--color-neon-cyan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glitch-skew {
|
||||||
|
0%, 100% {
|
||||||
|
transform: skew(0deg);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: skew(-2deg);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: skew(2deg);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: skew(-1deg);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: skew(1deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Neon pulse */
|
||||||
|
@keyframes neon-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
filter: drop-shadow(0 0 5px currentColor);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
filter: drop-shadow(0 0 20px currentColor) drop-shadow(0 0 40px currentColor);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes neon-border-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 5px var(--color-neon-green), inset 0 0 5px rgba(0, 255, 136, 0.1);
|
||||||
|
border-color: var(--color-neon-green);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 20px var(--color-neon-green), 0 0 40px var(--color-neon-green), inset 0 0 10px rgba(0, 255, 136, 0.2);
|
||||||
|
border-color: var(--color-neon-green);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scanline effect */
|
||||||
|
@keyframes scanline {
|
||||||
|
0% {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(100vh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blink cursor */
|
||||||
|
@keyframes blink-cursor {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flicker */
|
||||||
|
@keyframes flicker {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
92% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
93% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
94% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
96% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
97% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data stream */
|
||||||
|
@keyframes data-stream {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 0%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Float animation */
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rotate glow */
|
||||||
|
@keyframes rotate-glow {
|
||||||
|
0% {
|
||||||
|
filter: hue-rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
filter: hue-rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preserved animations from original */
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% {
|
0% {
|
||||||
background-position: -200% 0;
|
background-position: -200% 0;
|
||||||
@ -102,13 +255,13 @@ body {
|
|||||||
|
|
||||||
@keyframes pulse-ring {
|
@keyframes pulse-ring {
|
||||||
0% {
|
0% {
|
||||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);
|
box-shadow: 0 0 0 0 rgba(0, 255, 136, 0.7);
|
||||||
}
|
}
|
||||||
70% {
|
70% {
|
||||||
box-shadow: 0 0 0 10px rgba(34, 197, 94, 0);
|
box-shadow: 0 0 0 10px rgba(0, 255, 136, 0);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
box-shadow: 0 0 0 0 rgba(0, 255, 136, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +274,54 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Utility classes */
|
/* ========================================
|
||||||
|
ANIMATION UTILITY CLASSES
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.animate-glitch {
|
||||||
|
animation: glitch 0.3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-glitch-text {
|
||||||
|
animation: glitch-text 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-glitch-skew {
|
||||||
|
animation: glitch-skew 0.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-neon-pulse {
|
||||||
|
animation: neon-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-neon-border-pulse {
|
||||||
|
animation: neon-border-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-scanline {
|
||||||
|
animation: scanline 8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-blink-cursor {
|
||||||
|
animation: blink-cursor 1s step-end infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-flicker {
|
||||||
|
animation: flicker 4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-data-stream {
|
||||||
|
animation: data-stream 20s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: float 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-rotate-glow {
|
||||||
|
animation: rotate-glow 10s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.animate-shimmer {
|
.animate-shimmer {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
@ -145,15 +345,266 @@ body {
|
|||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focus visible styles for accessibility */
|
/* ========================================
|
||||||
.focus-visible-ring:focus-visible {
|
GLOW UTILITY CLASSES
|
||||||
outline: none;
|
======================================== */
|
||||||
ring: 2px;
|
|
||||||
ring-color: var(--color-ring);
|
.glow-green {
|
||||||
ring-offset: 2px;
|
box-shadow: var(--glow-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollbar styling */
|
.glow-green-intense {
|
||||||
|
box-shadow: var(--glow-green-intense);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-cyan {
|
||||||
|
box-shadow: var(--glow-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-cyan-intense {
|
||||||
|
box-shadow: var(--glow-cyan-intense);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-purple {
|
||||||
|
box-shadow: var(--glow-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-purple-intense {
|
||||||
|
box-shadow: var(--glow-purple-intense);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-pink {
|
||||||
|
box-shadow: var(--glow-pink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-yellow {
|
||||||
|
box-shadow: var(--glow-yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-orange {
|
||||||
|
box-shadow: var(--glow-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-glow-green {
|
||||||
|
text-shadow: 0 0 10px var(--color-neon-green), 0 0 20px var(--color-neon-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-glow-cyan {
|
||||||
|
text-shadow: 0 0 10px var(--color-neon-cyan), 0 0 20px var(--color-neon-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-glow-purple {
|
||||||
|
text-shadow: 0 0 10px var(--color-neon-purple), 0 0 20px var(--color-neon-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-glow-pink {
|
||||||
|
text-shadow: 0 0 10px var(--color-neon-pink), 0 0 20px var(--color-neon-pink);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
CYBER DECORATIVE CLASSES
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* Cyber grid background */
|
||||||
|
.cyber-grid {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(0, 255, 136, 0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(0, 255, 136, 0.03) 1px, transparent 1px);
|
||||||
|
background-size: 50px 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyber-grid-dense {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(0, 255, 136, 0.05) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(0, 255, 136, 0.05) 1px, transparent 1px);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scanline overlay */
|
||||||
|
.scanline-overlay {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanline-overlay::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
transparent,
|
||||||
|
transparent 2px,
|
||||||
|
rgba(0, 0, 0, 0.1) 2px,
|
||||||
|
rgba(0, 0, 0, 0.1) 4px
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corner accent brackets */
|
||||||
|
.corner-accent {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-accent::before,
|
||||||
|
.corner-accent::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-color: var(--color-neon-green);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-accent::before {
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
border-top-width: 2px;
|
||||||
|
border-left-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-accent::after {
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-right-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-accent:hover::before,
|
||||||
|
.corner-accent:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* All corners variant */
|
||||||
|
.corner-accent-all {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-accent-all::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
right: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--color-neon-green) 0%,
|
||||||
|
transparent 30%,
|
||||||
|
transparent 70%,
|
||||||
|
var(--color-neon-green) 100%
|
||||||
|
) 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cyber border with gradient */
|
||||||
|
.cyber-border {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyber-border::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -1px;
|
||||||
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--color-neon-green),
|
||||||
|
transparent,
|
||||||
|
var(--color-neon-cyan),
|
||||||
|
transparent,
|
||||||
|
var(--color-neon-purple)
|
||||||
|
);
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyber-border:hover::before {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Neon underline */
|
||||||
|
.neon-underline {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-underline::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--color-neon-green);
|
||||||
|
box-shadow: var(--glow-green);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-underline:hover::after,
|
||||||
|
.neon-underline.active::after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hexagon clip path */
|
||||||
|
.clip-hexagon {
|
||||||
|
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Diamond clip path */
|
||||||
|
.clip-diamond {
|
||||||
|
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chamfered corners (cut corners) */
|
||||||
|
.clip-chamfer {
|
||||||
|
clip-path: polygon(
|
||||||
|
12px 0,
|
||||||
|
calc(100% - 12px) 0,
|
||||||
|
100% 12px,
|
||||||
|
100% calc(100% - 12px),
|
||||||
|
calc(100% - 12px) 100%,
|
||||||
|
12px 100%,
|
||||||
|
0 calc(100% - 12px),
|
||||||
|
0 12px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
FONT UTILITY CLASSES
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.font-display {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-mono-cyber {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tech heading style */
|
||||||
|
.cyber-heading {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Terminal text */
|
||||||
|
.terminal-text {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
SCROLLBAR STYLING
|
||||||
|
======================================== */
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@ -165,25 +616,87 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: var(--color-muted-foreground);
|
background: var(--color-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--color-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--color-foreground);
|
background: var(--color-neon-green);
|
||||||
|
box-shadow: var(--glow-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Panel resize handle styling */
|
/* ========================================
|
||||||
|
PANEL RESIZE HANDLE STYLING
|
||||||
|
======================================== */
|
||||||
|
|
||||||
[data-panel-resize-handle-enabled] {
|
[data-panel-resize-handle-enabled] {
|
||||||
transition: background-color 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-panel-resize-handle-enabled]:hover {
|
[data-panel-resize-handle-enabled]:hover {
|
||||||
background-color: var(--color-primary) !important;
|
background-color: var(--color-neon-green) !important;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
box-shadow: var(--glow-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-panel-resize-handle-enabled][data-resize-handle-active] {
|
[data-panel-resize-handle-enabled][data-resize-handle-active] {
|
||||||
background-color: var(--color-primary) !important;
|
background-color: var(--color-neon-green) !important;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
box-shadow: var(--glow-green-intense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
FOCUS VISIBLE STYLES
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
.focus-visible-ring:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--color-background), 0 0 0 4px var(--color-neon-green), var(--glow-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
ACCESSIBILITY - REDUCED MOTION
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.animate-glitch,
|
||||||
|
.animate-glitch-text,
|
||||||
|
.animate-glitch-skew,
|
||||||
|
.animate-neon-pulse,
|
||||||
|
.animate-neon-border-pulse,
|
||||||
|
.animate-scanline,
|
||||||
|
.animate-blink-cursor,
|
||||||
|
.animate-flicker,
|
||||||
|
.animate-data-stream,
|
||||||
|
.animate-float,
|
||||||
|
.animate-rotate-glow,
|
||||||
|
.animate-shimmer,
|
||||||
|
.animate-shake,
|
||||||
|
.animate-pulse-ring,
|
||||||
|
.animate-spin {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
SELECTION STYLING
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: rgba(0, 255, 136, 0.3);
|
||||||
|
color: var(--color-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: rgba(0, 255, 136, 0.3);
|
||||||
|
color: var(--color-foreground);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,27 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import { JetBrains_Mono, Orbitron } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Navbar } from "@/components/Navbar";
|
import { Navbar } from "@/components/Navbar";
|
||||||
import { AuthProvider } from "@/lib/auth-context";
|
import { AuthProvider } from "@/lib/auth-context";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
|
|
||||||
|
// Cyberpunk fonts
|
||||||
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
|
subsets: ["latin", "cyrillic"],
|
||||||
|
variable: "--font-mono",
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
const orbitron = Orbitron({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-display",
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "ВолГУ.Контесты — Соревнования по программированию",
|
title: "ВолГУ.Контесты — Соревнования по программированию",
|
||||||
description: "Платформа для проведения соревнований по олимпиадному программированию от Волгоградского государственного университета",
|
description:
|
||||||
|
"Платформа для проведения соревнований по олимпиадному программированию от Волгоградского государственного университета",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -15,11 +30,17 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="ru">
|
<html lang="ru" className={`${jetbrainsMono.variable} ${orbitron.variable}`}>
|
||||||
<body className="min-h-screen bg-background antialiased">
|
<body className="min-h-screen bg-background antialiased font-mono">
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Navbar />
|
{/* Cyber grid background overlay */}
|
||||||
<main>{children}</main>
|
<div className="fixed inset-0 cyber-grid pointer-events-none z-0" />
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="relative z-10">
|
||||||
|
<Navbar />
|
||||||
|
<main>{children}</main>
|
||||||
|
</div>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
<Toaster
|
<Toaster
|
||||||
position="bottom-right"
|
position="bottom-right"
|
||||||
@ -27,8 +48,9 @@ export default function RootLayout({
|
|||||||
closeButton
|
closeButton
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
className: "font-sans",
|
className: "font-mono border border-border bg-card",
|
||||||
}}
|
}}
|
||||||
|
theme="dark"
|
||||||
/>
|
/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -11,7 +11,10 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { AlertError } from "@/components/ui/alert";
|
import { AlertError } from "@/components/ui/alert";
|
||||||
import { LogIn, Mail, Lock, Eye, EyeOff, Trophy } from "lucide-react";
|
import { LogIn, Mail, Lock, Eye, EyeOff, Terminal, ArrowRight } from "lucide-react";
|
||||||
|
import { GlowOrbs, GlitchText, CornerBrackets } from "@/components/decorative";
|
||||||
|
import { AuthIllustration } from "@/components/illustrations";
|
||||||
|
import { CyberBrandText, VolguLogo } from "@/components/VolguLogo";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
@ -39,103 +42,185 @@ export default function LoginPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-4rem)] flex items-center justify-center px-4">
|
<div className="min-h-[calc(100vh-4rem)] flex items-center justify-center px-4 relative overflow-hidden">
|
||||||
<motion.div
|
{/* Background effects */}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<GlowOrbs />
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<div className="absolute inset-0 cyber-grid opacity-20" />
|
||||||
transition={{ duration: 0.4 }}
|
|
||||||
className="w-full max-w-md"
|
{/* Decorative elements */}
|
||||||
>
|
<div className="absolute top-20 left-10 hidden lg:block">
|
||||||
<Card className="shadow-lg">
|
<motion.div
|
||||||
<CardHeader className="space-y-1 text-center pb-4">
|
animate={{ y: [0, -10, 0] }}
|
||||||
<motion.div
|
transition={{ duration: 3, repeat: Infinity }}
|
||||||
initial={{ scale: 0 }}
|
className="text-[var(--color-neon-green)]/20 font-mono text-xs"
|
||||||
animate={{ scale: 1 }}
|
>
|
||||||
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
|
{"// authenticating..."}
|
||||||
className="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center"
|
</motion.div>
|
||||||
>
|
</div>
|
||||||
<Trophy className="h-8 w-8 text-primary" />
|
<div className="absolute bottom-20 right-10 hidden lg:block">
|
||||||
</motion.div>
|
<motion.div
|
||||||
<CardTitle className="text-2xl font-bold">Вход</CardTitle>
|
animate={{ opacity: [0.2, 0.5, 0.2] }}
|
||||||
<p className="text-sm text-muted-foreground">
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
Войдите в свой аккаунт для участия в контестах
|
className="text-[var(--color-neon-cyan)]/30 font-mono text-xs"
|
||||||
|
>
|
||||||
|
{"<secure_connection/>"}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-5xl grid lg:grid-cols-2 gap-8 items-center relative z-10">
|
||||||
|
{/* Left side - Illustration (hidden on mobile) */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -30 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="hidden lg:block"
|
||||||
|
>
|
||||||
|
<AuthIllustration className="w-full max-w-md mx-auto" />
|
||||||
|
<div className="text-center mt-8">
|
||||||
|
<p className="text-muted-foreground font-mono text-sm">
|
||||||
|
<span className="text-[var(--color-neon-green)]">$</span> Безопасный вход в систему
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent>
|
</motion.div>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
{error && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: "auto" }}
|
|
||||||
>
|
|
||||||
<AlertError>{error}</AlertError>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
{/* Right side - Login form */}
|
||||||
<Label htmlFor="email">Email</Label>
|
<motion.div
|
||||||
<div className="relative">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<Input
|
transition={{ duration: 0.4 }}
|
||||||
id="email"
|
className="w-full max-w-md mx-auto"
|
||||||
type="email"
|
>
|
||||||
value={email}
|
<Card variant="cyber" className="relative overflow-hidden">
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
{/* Card glow effect */}
|
||||||
required
|
<div className="absolute inset-0 bg-gradient-to-br from-[var(--color-neon-green)]/5 via-transparent to-[var(--color-neon-cyan)]/5" />
|
||||||
className="pl-10"
|
|
||||||
placeholder="email@example.com"
|
<CardHeader className="space-y-1 text-center pb-4 relative">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
|
||||||
|
className="relative mx-auto mb-4"
|
||||||
|
>
|
||||||
|
<div className="w-16 h-16 rounded-full border-2 border-[var(--color-neon-green)] flex items-center justify-center relative">
|
||||||
|
<VolguLogo className="h-8 w-8" animated />
|
||||||
|
{/* Animated ring */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 rounded-full border border-[var(--color-neon-green)]/50"
|
||||||
|
animate={{ scale: [1, 1.2, 1], opacity: [0.5, 0, 0.5] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<CardTitle className="text-2xl font-display font-bold">
|
||||||
<Label htmlFor="password">Пароль</Label>
|
<GlitchText text="АВТОРИЗАЦИЯ" intensity="low" />
|
||||||
<div className="relative">
|
</CardTitle>
|
||||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<p className="text-sm text-muted-foreground font-mono">
|
||||||
<Input
|
<span className="text-[var(--color-neon-cyan)]">></span> Войдите для участия в контестах
|
||||||
id="password"
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
className="pl-10 pr-10"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<EyeOff className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full" loading={isLoading}>
|
|
||||||
<LogIn className="h-4 w-4 mr-2" />
|
|
||||||
Войти
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Нет аккаунта?{" "}
|
|
||||||
<Link
|
|
||||||
href="/register"
|
|
||||||
className="text-primary hover:underline font-medium"
|
|
||||||
>
|
|
||||||
Зарегистрироваться
|
|
||||||
</Link>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</CardHeader>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
<CardContent className="relative">
|
||||||
</motion.div>
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
|
>
|
||||||
|
<AlertError>{error}</AlertError>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<div className="relative group">
|
||||||
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-[var(--color-neon-green)] transition-colors" />
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
className="pl-10 font-mono"
|
||||||
|
placeholder="user@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
|
Пароль
|
||||||
|
</Label>
|
||||||
|
<div className="relative group">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-[var(--color-neon-green)] transition-colors" />
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="pl-10 pr-10 font-mono"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-[var(--color-neon-green)] transition-colors"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" variant="cyber" className="w-full group" loading={isLoading}>
|
||||||
|
<LogIn className="h-4 w-4 mr-2" />
|
||||||
|
Войти в систему
|
||||||
|
<ArrowRight className="h-4 w-4 ml-2 transition-transform group-hover:translate-x-1" />
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="relative my-6">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-[var(--color-border)]" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-xs">
|
||||||
|
<span className="px-2 bg-[var(--color-card)] text-muted-foreground font-mono">
|
||||||
|
или
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-muted-foreground font-mono">
|
||||||
|
Нет аккаунта?{" "}
|
||||||
|
<Link
|
||||||
|
href="/register"
|
||||||
|
className="text-[var(--color-neon-cyan)] hover:text-[var(--color-neon-green)] transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Зарегистрироваться
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom decoration */}
|
||||||
|
<div className="mt-6 pt-4 border-t border-[var(--color-border)]">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Terminal className="h-4 w-4 text-[var(--color-neon-green)]" />
|
||||||
|
<CyberBrandText className="text-xs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,65 +4,72 @@ import Link from "next/link";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { useAuth } from "@/lib/auth-context";
|
import { useAuth } from "@/lib/auth-context";
|
||||||
import {
|
import {
|
||||||
Trophy,
|
Trophy,
|
||||||
Zap,
|
Zap,
|
||||||
Code2,
|
Code2,
|
||||||
Users,
|
Users,
|
||||||
CheckCircle2,
|
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Terminal,
|
Terminal,
|
||||||
Timer,
|
Timer,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
|
Cpu,
|
||||||
|
Shield,
|
||||||
|
Sparkles,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { VolguLogo } from "@/components/VolguLogo";
|
import { CyberBrandText, VolguLogo } from "@/components/VolguLogo";
|
||||||
|
import { GlowOrbs, GlitchText, CornerBrackets, TypewriterText } from "@/components/decorative";
|
||||||
|
import { HeroIllustration } from "@/components/illustrations";
|
||||||
|
import { MatrixRain } from "@/components/effects";
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
icon: Trophy,
|
icon: Trophy,
|
||||||
title: "Соревнования",
|
title: "Соревнования",
|
||||||
description: "Участвуйте в контестах и соревнуйтесь с другими программистами",
|
description: "Участвуйте в контестах и соревнуйтесь с другими программистами",
|
||||||
color: "text-yellow-500",
|
color: "var(--color-neon-green)",
|
||||||
bgColor: "bg-yellow-500/10",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Zap,
|
icon: Zap,
|
||||||
title: "Автопроверка",
|
title: "Автопроверка",
|
||||||
description: "Мгновенная проверка решений с детальными результатами по каждому тесту",
|
description: "Мгновенная проверка решений с детальными результатами по каждому тесту",
|
||||||
color: "text-blue-500",
|
color: "var(--color-neon-cyan)",
|
||||||
bgColor: "bg-blue-500/10",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Code2,
|
icon: Code2,
|
||||||
title: "30+ языков",
|
title: "30+ языков",
|
||||||
description: "Python, C++, Java, JavaScript, Go, Rust и многие другие языки",
|
description: "Python, C++, Java, JavaScript, Go, Rust и многие другие языки",
|
||||||
color: "text-green-500",
|
color: "var(--color-neon-purple)",
|
||||||
bgColor: "bg-green-500/10",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Timer,
|
icon: Timer,
|
||||||
title: "Real-time таймеры",
|
title: "Real-time таймеры",
|
||||||
description: "Следите за временем контеста и оставшимся временем в реальном времени",
|
description: "Следите за временем контеста и оставшимся временем в реальном времени",
|
||||||
color: "text-orange-500",
|
color: "var(--color-neon-orange)",
|
||||||
bgColor: "bg-orange-500/10",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
title: "Рейтинг",
|
title: "Рейтинг",
|
||||||
description: "Отслеживайте свой прогресс и соревнуйтесь в таблице лидеров",
|
description: "Отслеживайте свой прогресс и соревнуйтесь в таблице лидеров",
|
||||||
color: "text-purple-500",
|
color: "var(--color-neon-pink)",
|
||||||
bgColor: "bg-purple-500/10",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Terminal,
|
icon: Terminal,
|
||||||
title: "Удобный редактор",
|
title: "Удобный редактор",
|
||||||
description: "Современный редактор кода с подсветкой синтаксиса и автодополнением",
|
description: "Современный редактор кода с подсветкой синтаксиса и автодополнением",
|
||||||
color: "text-pink-500",
|
color: "var(--color-neon-yellow)",
|
||||||
bgColor: "bg-pink-500/10",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ value: "30+", label: "Языков", color: "var(--color-neon-green)" },
|
||||||
|
{ value: "∞", label: "Контестов", color: "var(--color-neon-cyan)" },
|
||||||
|
{ value: "100%", label: "Автопроверка", color: "var(--color-neon-purple)" },
|
||||||
|
{ value: "24/7", label: "Доступность", color: "var(--color-neon-pink)" },
|
||||||
|
];
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
visible: {
|
visible: {
|
||||||
@ -82,109 +89,164 @@ export default function HomePage() {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-4rem)]">
|
<div className="min-h-[calc(100vh-4rem)] relative">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="relative overflow-hidden">
|
<section className="relative overflow-hidden min-h-[90vh] flex items-center">
|
||||||
{/* Background gradient */}
|
{/* Background Effects */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-primary/5" />
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_20%,rgba(120,119,198,0.1),transparent_50%)]" />
|
<MatrixRain density="light" color="green" speed="slow" />
|
||||||
|
|
||||||
<div className="container mx-auto px-4 py-24 relative">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 30 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.6 }}
|
|
||||||
className="text-center max-w-3xl mx-auto"
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
initial={{ scale: 0 }}
|
|
||||||
animate={{ scale: 1 }}
|
|
||||||
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
|
|
||||||
className="inline-flex items-center gap-3 px-4 py-2 rounded-full bg-[#2B4B7C]/10 mb-6"
|
|
||||||
>
|
|
||||||
<VolguLogo className="h-6 w-6" />
|
|
||||||
<span className="text-sm font-medium text-[#2B4B7C]">Волгоградский государственный университет</span>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<h1 className="text-4xl md:text-6xl font-bold mb-6">
|
|
||||||
<span className="text-[#2B4B7C]">ВолГУ</span>
|
|
||||||
<span className="text-muted-foreground">.</span>
|
|
||||||
<span className="text-primary">Контесты</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
|
|
||||||
Платформа для проведения соревнований по олимпиадному программированию
|
|
||||||
от Волгоградского государственного университета.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.4 }}
|
|
||||||
className="flex flex-col sm:flex-row gap-4 justify-center"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href="/contests"
|
|
||||||
className="inline-flex items-center justify-center gap-2 h-11 rounded-lg px-8 text-lg font-medium bg-primary text-primary-foreground shadow hover:bg-primary/90 transition-all"
|
|
||||||
>
|
|
||||||
<Trophy className="h-5 w-5" />
|
|
||||||
Перейти к контестам
|
|
||||||
<ArrowRight className="h-5 w-5" />
|
|
||||||
</Link>
|
|
||||||
{!user && (
|
|
||||||
<Link
|
|
||||||
href="/register"
|
|
||||||
className="inline-flex items-center justify-center gap-2 h-11 rounded-lg px-8 text-lg font-medium border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground transition-all"
|
|
||||||
>
|
|
||||||
<Users className="h-5 w-5" />
|
|
||||||
Создать аккаунт
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 40 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.6 }}
|
|
||||||
className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8 max-w-4xl mx-auto"
|
|
||||||
>
|
|
||||||
{[
|
|
||||||
{ value: "30+", label: "Языков программирования" },
|
|
||||||
{ value: "∞", label: "Контестов" },
|
|
||||||
{ value: "100%", label: "Автопроверка" },
|
|
||||||
{ value: "24/7", label: "Доступность" },
|
|
||||||
].map((stat, index) => (
|
|
||||||
<div key={index} className="text-center">
|
|
||||||
<div className="text-3xl md:text-4xl font-bold text-primary mb-1">
|
|
||||||
{stat.value}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<GlowOrbs />
|
||||||
|
|
||||||
|
{/* Cyber grid overlay */}
|
||||||
|
<div className="absolute inset-0 cyber-grid opacity-30" />
|
||||||
|
|
||||||
|
{/* Scanline effect */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-[var(--color-neon-green)]/[0.02] to-transparent animate-scanline" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 py-16 relative z-10">
|
||||||
|
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||||
|
{/* Left column - Text content */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -30 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
>
|
||||||
|
{/* University badge */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="inline-flex items-center gap-3 px-4 py-2 rounded-lg bg-[var(--color-card)] border border-[var(--color-neon-green)]/30 mb-8"
|
||||||
|
>
|
||||||
|
<VolguLogo className="h-5 w-5" animated />
|
||||||
|
<span className="text-sm font-mono text-[var(--color-neon-green)]">
|
||||||
|
<TypewriterText text="Волгоградский государственный университет" speed={30} />
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Main heading */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="mb-6"
|
||||||
|
>
|
||||||
|
<GlitchText
|
||||||
|
text=">VOLGU.CONTESTS"
|
||||||
|
className="text-4xl md:text-5xl lg:text-6xl font-display font-bold"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Subtitle with terminal style */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.4 }}
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<p className="text-lg md:text-xl text-muted-foreground font-mono leading-relaxed">
|
||||||
|
<span className="text-[var(--color-neon-cyan)]">$</span> Платформа для проведения соревнований по{" "}
|
||||||
|
<span className="text-[var(--color-neon-green)] text-glow-green">
|
||||||
|
олимпиадному программированию
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground font-mono mt-2 flex items-center gap-2">
|
||||||
|
<span className="text-[var(--color-neon-purple)]">></span>
|
||||||
|
<span className="animate-blink-cursor">_</span>
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* CTA Buttons */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="flex flex-col sm:flex-row gap-4"
|
||||||
|
>
|
||||||
|
<Button asChild variant="cyber" size="lg" className="group">
|
||||||
|
<Link href="/contests">
|
||||||
|
<Trophy className="h-5 w-5 mr-2" />
|
||||||
|
Перейти к контестам
|
||||||
|
<ArrowRight className="h-5 w-5 ml-2 transition-transform group-hover:translate-x-1" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
{!user && (
|
||||||
|
<Button asChild variant="neon" size="lg">
|
||||||
|
<Link href="/register">
|
||||||
|
<Users className="h-5 w-5 mr-2" />
|
||||||
|
Создать аккаунт
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.7 }}
|
||||||
|
className="mt-12 grid grid-cols-2 md:grid-cols-4 gap-6"
|
||||||
|
>
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<div key={index} className="relative group">
|
||||||
|
<CornerBrackets color={stat.color} size="sm" />
|
||||||
|
<div className="text-center py-4">
|
||||||
|
<div
|
||||||
|
className="text-2xl md:text-3xl font-display font-bold mb-1"
|
||||||
|
style={{ color: stat.color, textShadow: `0 0 20px ${stat.color}` }}
|
||||||
|
>
|
||||||
|
{stat.value}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-mono text-muted-foreground uppercase tracking-wider">
|
||||||
|
{stat.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Right column - Illustration */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: 30 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.3 }}
|
||||||
|
className="hidden lg:flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<HeroIllustration className="w-full max-w-lg" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom gradient fade */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-[var(--color-background)] to-transparent" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Features Section */}
|
{/* Features Section */}
|
||||||
<section className="py-24 bg-muted/30">
|
<section className="py-24 relative">
|
||||||
<div className="container mx-auto px-4">
|
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-[var(--color-neon-purple)]/5 to-transparent" />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 relative">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="text-center mb-16"
|
className="text-center mb-16"
|
||||||
>
|
>
|
||||||
<Badge variant="outline" className="mb-4">
|
<Badge variant="cyan" className="mb-4 font-mono">
|
||||||
Возможности
|
<Cpu className="h-3 w-3 mr-1" />
|
||||||
|
SYSTEM.FEATURES
|
||||||
</Badge>
|
</Badge>
|
||||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
<h2 className="text-3xl md:text-4xl font-display font-bold mb-4">
|
||||||
Всё, что нужно для соревнований
|
<span className="text-[var(--color-neon-green)]"><</span>
|
||||||
|
Возможности платформы
|
||||||
|
<span className="text-[var(--color-neon-green)]">/></span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
<p className="text-lg text-muted-foreground font-mono max-w-2xl mx-auto">
|
||||||
Наша платформа предоставляет все необходимые инструменты для проведения
|
Все необходимые инструменты для проведения и участия в соревнованиях
|
||||||
и участия в соревнованиях по программированию
|
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@ -199,15 +261,44 @@ export default function HomePage() {
|
|||||||
const Icon = feature.icon;
|
const Icon = feature.icon;
|
||||||
return (
|
return (
|
||||||
<motion.div key={index} variants={itemVariants}>
|
<motion.div key={index} variants={itemVariants}>
|
||||||
<Card className="h-full hover:shadow-lg transition-shadow">
|
<Card variant="cyber" className="h-full group hover:border-[var(--color-neon-green)]/50 transition-all duration-300">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6 relative">
|
||||||
|
{/* Icon with glow */}
|
||||||
<div
|
<div
|
||||||
className={`w-12 h-12 rounded-lg ${feature.bgColor} flex items-center justify-center mb-4`}
|
className="w-14 h-14 rounded-lg flex items-center justify-center mb-4 transition-all duration-300 group-hover:scale-110"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `color-mix(in srgb, ${feature.color} 15%, transparent)`,
|
||||||
|
border: `1px solid ${feature.color}40`,
|
||||||
|
boxShadow: `0 0 20px ${feature.color}20`
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon className={`h-6 w-6 ${feature.color}`} />
|
<Icon
|
||||||
|
className="h-7 w-7 transition-all duration-300"
|
||||||
|
style={{ color: feature.color }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold mb-2">{feature.title}</h3>
|
|
||||||
<p className="text-muted-foreground">{feature.description}</p>
|
{/* Title */}
|
||||||
|
<h3
|
||||||
|
className="text-lg font-display font-semibold mb-2 transition-colors duration-300"
|
||||||
|
style={{ color: feature.color }}
|
||||||
|
>
|
||||||
|
{feature.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-muted-foreground font-mono text-sm leading-relaxed">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Hover effect line */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-0 right-0 h-[2px] opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(90deg, transparent, ${feature.color}, transparent)`,
|
||||||
|
boxShadow: `0 0 10px ${feature.color}`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -218,32 +309,76 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<section className="py-24">
|
<section className="py-24 relative">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
whileInView={{ opacity: 1, scale: 1 }}
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-primary to-primary/80 p-12 text-center text-primary-foreground"
|
className="relative overflow-hidden rounded-2xl"
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_70%_30%,rgba(255,255,255,0.1),transparent_50%)]" />
|
{/* Background with cyber styling */}
|
||||||
<div className="relative">
|
<div className="absolute inset-0 bg-gradient-to-r from-[var(--color-neon-green)]/10 via-[var(--color-neon-cyan)]/10 to-[var(--color-neon-purple)]/10" />
|
||||||
<CheckCircle2 className="h-12 w-12 mx-auto mb-6 opacity-90" />
|
<div className="absolute inset-0 cyber-grid opacity-20" />
|
||||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
|
||||||
Готовы начать?
|
{/* Border glow */}
|
||||||
|
<div className="absolute inset-0 rounded-2xl border border-[var(--color-neon-green)]/30" />
|
||||||
|
<div className="absolute inset-[1px] rounded-2xl border border-[var(--color-neon-cyan)]/20" />
|
||||||
|
|
||||||
|
{/* Animated corner accents */}
|
||||||
|
<div className="absolute top-0 left-0 w-20 h-20">
|
||||||
|
<div className="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--color-neon-green)] to-transparent" />
|
||||||
|
<div className="absolute top-0 left-0 h-full w-[2px] bg-gradient-to-b from-[var(--color-neon-green)] to-transparent" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-0 right-0 w-20 h-20">
|
||||||
|
<div className="absolute top-0 right-0 w-full h-[2px] bg-gradient-to-l from-[var(--color-neon-cyan)] to-transparent" />
|
||||||
|
<div className="absolute top-0 right-0 h-full w-[2px] bg-gradient-to-b from-[var(--color-neon-cyan)] to-transparent" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-0 left-0 w-20 h-20">
|
||||||
|
<div className="absolute bottom-0 left-0 w-full h-[2px] bg-gradient-to-r from-[var(--color-neon-purple)] to-transparent" />
|
||||||
|
<div className="absolute bottom-0 left-0 h-full w-[2px] bg-gradient-to-t from-[var(--color-neon-purple)] to-transparent" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-0 right-0 w-20 h-20">
|
||||||
|
<div className="absolute bottom-0 right-0 w-full h-[2px] bg-gradient-to-l from-[var(--color-neon-pink)] to-transparent" />
|
||||||
|
<div className="absolute bottom-0 right-0 h-full w-[2px] bg-gradient-to-t from-[var(--color-neon-pink)] to-transparent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="relative p-12 text-center">
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
boxShadow: [
|
||||||
|
"0 0 20px var(--color-neon-green)",
|
||||||
|
"0 0 40px var(--color-neon-green)",
|
||||||
|
"0 0 20px var(--color-neon-green)"
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
|
className="w-16 h-16 mx-auto mb-6 rounded-full border-2 border-[var(--color-neon-green)] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Sparkles className="h-8 w-8 text-[var(--color-neon-green)]" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<h2 className="text-3xl md:text-4xl font-display font-bold mb-4">
|
||||||
|
<span className="text-[var(--color-neon-green)]">Готовы</span>{" "}
|
||||||
|
<span className="text-foreground">начать</span>
|
||||||
|
<span className="text-[var(--color-neon-cyan)]">?</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg opacity-90 mb-8 max-w-xl mx-auto">
|
|
||||||
Присоединяйтесь к платформе ВолГУ.Контесты и участвуйте
|
<p className="text-lg text-muted-foreground font-mono mb-8 max-w-xl mx-auto">
|
||||||
в соревнованиях по олимпиадному программированию.
|
Присоединяйтесь к платформе{" "}
|
||||||
|
<span className="text-[var(--color-neon-green)]">>VOLGU.CONTESTS</span>{" "}
|
||||||
|
и участвуйте в соревнованиях
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<Link
|
<Button asChild variant="cyber" size="lg" className="group">
|
||||||
href={user ? "/contests" : "/register"}
|
<Link href={user ? "/contests" : "/register"}>
|
||||||
className="inline-flex items-center justify-center gap-2 h-11 rounded-lg px-8 text-lg font-medium bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 transition-all"
|
<Shield className="h-5 w-5 mr-2" />
|
||||||
>
|
{user ? "Перейти к контестам" : "Начать сейчас"}
|
||||||
{user ? "Перейти к контестам" : "Начать бесплатно"}
|
<ArrowRight className="h-5 w-5 ml-2 transition-transform group-hover:translate-x-1" />
|
||||||
<ArrowRight className="h-5 w-5" />
|
</Link>
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -251,9 +386,28 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="border-t border-border py-8">
|
<footer className="relative border-t border-[var(--color-border)] py-8">
|
||||||
<div className="container mx-auto px-4 text-center text-sm text-muted-foreground">
|
{/* Top border glow */}
|
||||||
<p>ВолГУ.Контесты © {new Date().getFullYear()} — Волгоградский государственный университет</p>
|
<div className="absolute top-0 left-0 right-0 h-[1px] bg-gradient-to-r from-transparent via-[var(--color-neon-green)]/30 to-transparent" />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<VolguLogo className="h-6 w-6" />
|
||||||
|
<CyberBrandText className="text-sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-muted-foreground font-mono text-center">
|
||||||
|
<span className="text-[var(--color-neon-green)]">©</span> {new Date().getFullYear()}{" "}
|
||||||
|
<span className="text-muted-foreground">—</span>{" "}
|
||||||
|
Волгоградский государственный университет
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-xs font-mono text-muted-foreground">
|
||||||
|
<span className="inline-block w-2 h-2 rounded-full bg-[var(--color-neon-green)] animate-pulse" />
|
||||||
|
<span>SYSTEM.ONLINE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -21,7 +21,11 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Save,
|
Save,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Shield,
|
||||||
|
Send,
|
||||||
|
Globe,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { GlowOrbs, GlitchText } from "@/components/decorative";
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||||||
|
|
||||||
@ -126,8 +130,11 @@ export default function ProfilePage() {
|
|||||||
if (authLoading) {
|
if (authLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8 max-w-2xl">
|
<div className="container mx-auto px-4 py-8 max-w-2xl">
|
||||||
<Skeleton className="h-10 w-48 mb-8" />
|
<div className="flex items-center gap-4 mb-8">
|
||||||
<Skeleton className="h-96 rounded-xl" />
|
<Skeleton className="h-12 w-12 rounded-lg" />
|
||||||
|
<Skeleton className="h-10 w-48" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-[600px] rounded-xl" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -141,196 +148,270 @@ export default function ProfilePage() {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8 max-w-2xl">
|
<div className="min-h-[calc(100vh-4rem)] relative">
|
||||||
<motion.div
|
{/* Background */}
|
||||||
initial={{ opacity: 0, y: -20 }}
|
<GlowOrbs />
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<div className="absolute inset-0 cyber-grid opacity-10" />
|
||||||
className="mb-8"
|
|
||||||
>
|
|
||||||
<h1 className="text-3xl font-bold flex items-center gap-3">
|
|
||||||
<User className="h-8 w-8 text-primary" />
|
|
||||||
Профиль
|
|
||||||
</h1>
|
|
||||||
<p className="text-muted-foreground mt-1">
|
|
||||||
Управляйте своими данными
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
<div className="container mx-auto px-4 py-8 max-w-2xl relative">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
{/* Header */}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<motion.div
|
||||||
transition={{ delay: 0.1 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<Card>
|
className="mb-8"
|
||||||
<CardHeader>
|
>
|
||||||
<CardTitle>Личные данные</CardTitle>
|
<div className="flex items-center gap-4">
|
||||||
</CardHeader>
|
<div className="w-12 h-12 rounded-lg bg-[var(--color-neon-cyan)]/10 border border-[var(--color-neon-cyan)]/30 flex items-center justify-center">
|
||||||
<CardContent>
|
<User className="h-6 w-6 text-[var(--color-neon-cyan)]" />
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
</div>
|
||||||
{/* Avatar Section */}
|
<div>
|
||||||
<div className="flex flex-col items-center gap-4 pb-6 border-b border-border">
|
<h1 className="text-3xl font-display font-bold">
|
||||||
<div className="relative group">
|
<GlitchText text="ПРОФИЛЬ" intensity="low" color="var(--color-neon-cyan)" />
|
||||||
<div
|
</h1>
|
||||||
className="w-32 h-32 rounded-full overflow-hidden bg-muted flex items-center justify-center cursor-pointer border-4 border-background shadow-lg"
|
<p className="text-sm font-mono text-muted-foreground">
|
||||||
onClick={handleAvatarClick}
|
<span className="text-[var(--color-neon-green)]">$</span> user.settings.edit()
|
||||||
>
|
</p>
|
||||||
{uploadingAvatar ? (
|
</div>
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
</div>
|
||||||
) : avatarUrl ? (
|
</motion.div>
|
||||||
<img
|
|
||||||
src={avatarUrl}
|
<motion.div
|
||||||
alt="Avatar"
|
initial={{ opacity: 0, y: 20 }}
|
||||||
className="w-full h-full object-cover"
|
animate={{ opacity: 1, y: 0 }}
|
||||||
/>
|
transition={{ delay: 0.1 }}
|
||||||
) : (
|
>
|
||||||
<User className="h-16 w-16 text-muted-foreground" />
|
<Card variant="cyber" className="relative overflow-hidden">
|
||||||
)}
|
{/* Top accent */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-[var(--color-neon-cyan)]/50 to-transparent" />
|
||||||
|
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="font-display flex items-center gap-2">
|
||||||
|
<Shield className="h-5 w-5 text-[var(--color-neon-cyan)]" />
|
||||||
|
ЛИЧНЫЕ ДАННЫЕ
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Avatar Section */}
|
||||||
|
<div className="flex flex-col items-center gap-4 pb-6 border-b border-[var(--color-border)]">
|
||||||
|
<div className="relative group">
|
||||||
|
{/* Avatar ring glow */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute -inset-1 rounded-full opacity-50"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(45deg, var(--color-neon-green), var(--color-neon-cyan))`,
|
||||||
|
filter: "blur(8px)",
|
||||||
|
}}
|
||||||
|
animate={{ opacity: [0.3, 0.5, 0.3] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="relative w-32 h-32 rounded-full overflow-hidden bg-[var(--color-card)] flex items-center justify-center cursor-pointer border-2 border-[var(--color-neon-cyan)]"
|
||||||
|
onClick={handleAvatarClick}
|
||||||
|
style={{
|
||||||
|
boxShadow: "0 0 20px var(--color-neon-cyan)40",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{uploadingAvatar ? (
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-[var(--color-neon-cyan)]" />
|
||||||
|
) : avatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={avatarUrl}
|
||||||
|
alt="Avatar"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<User className="h-16 w-16 text-[var(--color-neon-cyan)]/50" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAvatarClick}
|
||||||
|
className="absolute bottom-0 right-0 p-2 bg-[var(--color-neon-cyan)] text-[var(--color-background)] rounded-full shadow-lg hover:bg-[var(--color-neon-green)] transition-colors"
|
||||||
|
style={{
|
||||||
|
boxShadow: "0 0 15px var(--color-neon-cyan)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Camera className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<input
|
||||||
type="button"
|
ref={fileInputRef}
|
||||||
onClick={handleAvatarClick}
|
type="file"
|
||||||
className="absolute bottom-0 right-0 p-2 bg-primary text-primary-foreground rounded-full shadow-lg hover:bg-primary/90 transition-colors"
|
accept="image/*"
|
||||||
>
|
onChange={handleAvatarChange}
|
||||||
<Camera className="h-4 w-4" />
|
className="hidden"
|
||||||
</button>
|
/>
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleAvatarChange}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleAvatarClick}
|
|
||||||
disabled={uploadingAvatar}
|
|
||||||
>
|
|
||||||
<Camera className="h-4 w-4 mr-2" />
|
|
||||||
Загрузить фото
|
|
||||||
</Button>
|
|
||||||
{user.avatar_url && (
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="neon"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleDeleteAvatar}
|
onClick={handleAvatarClick}
|
||||||
className="text-destructive hover:text-destructive"
|
disabled={uploadingAvatar}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
<Camera className="h-4 w-4 mr-2" />
|
||||||
Удалить
|
Загрузить фото
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
{user.avatar_url && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDeleteAvatar}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Удалить
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
|
// JPG, PNG или GIF. Max 5MB.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
JPG, PNG или GIF. Максимум 5MB.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Email (read-only) */}
|
{/* Email (read-only) */}
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="email">Email</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
value={user.email}
|
|
||||||
disabled
|
|
||||||
icon={<Mail className="h-4 w-4" />}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Email нельзя изменить
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Username */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="username">Имя пользователя</Label>
|
|
||||||
<Input
|
|
||||||
id="username"
|
|
||||||
name="username"
|
|
||||||
value={formData.username}
|
|
||||||
onChange={handleChange}
|
|
||||||
icon={<AtSign className="h-4 w-4" />}
|
|
||||||
placeholder="username"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Full Name */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="full_name">ФИО</Label>
|
|
||||||
<Input
|
|
||||||
id="full_name"
|
|
||||||
name="full_name"
|
|
||||||
value={formData.full_name}
|
|
||||||
onChange={handleChange}
|
|
||||||
icon={<User className="h-4 w-4" />}
|
|
||||||
placeholder="Иванов Иван Иванович"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Study Group */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="study_group">Учебная группа</Label>
|
|
||||||
<Input
|
|
||||||
id="study_group"
|
|
||||||
name="study_group"
|
|
||||||
value={formData.study_group}
|
|
||||||
onChange={handleChange}
|
|
||||||
icon={<GraduationCap className="h-4 w-4" />}
|
|
||||||
placeholder="ПМИб-241"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Social Links */}
|
|
||||||
<div className="space-y-4 pt-4 border-t border-border">
|
|
||||||
<h3 className="font-medium">Социальные сети</h3>
|
|
||||||
|
|
||||||
{/* Telegram */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="telegram">Telegram</Label>
|
<Label htmlFor="email" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
<Input
|
Email
|
||||||
id="telegram"
|
</Label>
|
||||||
name="telegram"
|
<div className="relative">
|
||||||
value={formData.telegram}
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
onChange={handleChange}
|
<Input
|
||||||
placeholder="@username или ссылка"
|
id="email"
|
||||||
/>
|
value={user.email}
|
||||||
|
disabled
|
||||||
|
className="pl-10 font-mono opacity-60"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
|
<span className="text-[var(--color-neon-orange)]">!</span> Email нельзя изменить
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* VK */}
|
{/* Username */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="vk">VK</Label>
|
<Label htmlFor="username" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
<Input
|
Имя пользователя
|
||||||
id="vk"
|
</Label>
|
||||||
name="vk"
|
<div className="relative group">
|
||||||
value={formData.vk}
|
<AtSign className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-[var(--color-neon-green)] transition-colors" />
|
||||||
onChange={handleChange}
|
<Input
|
||||||
placeholder="@username или ссылка"
|
id="username"
|
||||||
/>
|
name="username"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="pl-10 font-mono"
|
||||||
|
placeholder="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Role Badge */}
|
{/* Full Name */}
|
||||||
<div className="pt-4 border-t border-border">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<Label htmlFor="full_name" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
<span className="text-sm text-muted-foreground">Роль</span>
|
ФИО
|
||||||
<Badge variant={user.role === "admin" ? "default" : "secondary"}>
|
</Label>
|
||||||
{user.role === "admin" ? "Администратор" : "Участник"}
|
<div className="relative group">
|
||||||
</Badge>
|
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-[var(--color-neon-green)] transition-colors" />
|
||||||
|
<Input
|
||||||
|
id="full_name"
|
||||||
|
name="full_name"
|
||||||
|
value={formData.full_name}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="pl-10 font-mono"
|
||||||
|
placeholder="Иванов Иван Иванович"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Study Group */}
|
||||||
<Button type="submit" className="w-full" loading={saving}>
|
<div className="space-y-2">
|
||||||
<Save className="h-4 w-4 mr-2" />
|
<Label htmlFor="study_group" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
Сохранить изменения
|
Учебная группа
|
||||||
</Button>
|
</Label>
|
||||||
</form>
|
<div className="relative group">
|
||||||
</CardContent>
|
<GraduationCap className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-[var(--color-neon-green)] transition-colors" />
|
||||||
</Card>
|
<Input
|
||||||
</motion.div>
|
id="study_group"
|
||||||
|
name="study_group"
|
||||||
|
value={formData.study_group}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="pl-10 font-mono"
|
||||||
|
placeholder="ПМИб-241"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social Links */}
|
||||||
|
<div className="space-y-4 pt-6 border-t border-[var(--color-border)]">
|
||||||
|
<h3 className="font-display font-medium flex items-center gap-2 text-[var(--color-neon-purple)]">
|
||||||
|
<Globe className="h-4 w-4" />
|
||||||
|
СОЦИАЛЬНЫЕ СЕТИ
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Telegram */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="telegram" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
|
Telegram
|
||||||
|
</Label>
|
||||||
|
<div className="relative group">
|
||||||
|
<Send className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-[var(--color-neon-cyan)] transition-colors" />
|
||||||
|
<Input
|
||||||
|
id="telegram"
|
||||||
|
name="telegram"
|
||||||
|
value={formData.telegram}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="pl-10 font-mono"
|
||||||
|
placeholder="@username или ссылка"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* VK */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="vk" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
|
VK
|
||||||
|
</Label>
|
||||||
|
<div className="relative group">
|
||||||
|
<Globe className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-[var(--color-neon-cyan)] transition-colors" />
|
||||||
|
<Input
|
||||||
|
id="vk"
|
||||||
|
name="vk"
|
||||||
|
value={formData.vk}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="pl-10 font-mono"
|
||||||
|
placeholder="@username или ссылка"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Role Badge */}
|
||||||
|
<div className="pt-6 border-t border-[var(--color-border)]">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground font-mono uppercase tracking-wider">
|
||||||
|
Роль в системе
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
variant={user.role === "admin" ? "purple" : "outline"}
|
||||||
|
className="font-mono"
|
||||||
|
>
|
||||||
|
{user.role === "admin" ? "ADMIN" : "USER"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<Button type="submit" variant="cyber" className="w-full group" loading={saving}>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
Сохранить изменения
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,10 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { AlertError } from "@/components/ui/alert";
|
import { AlertError } from "@/components/ui/alert";
|
||||||
import { UserPlus, Mail, Lock, Eye, EyeOff, User, Trophy, Check, X } from "lucide-react";
|
import { UserPlus, Mail, Lock, Eye, EyeOff, User, Check, X, Terminal, ArrowRight, Shield } from "lucide-react";
|
||||||
|
import { GlowOrbs, GlitchText } from "@/components/decorative";
|
||||||
|
import { AuthIllustration } from "@/components/illustrations";
|
||||||
|
import { CyberBrandText, VolguLogo } from "@/components/VolguLogo";
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
@ -58,155 +61,251 @@ export default function RegisterPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[calc(100vh-4rem)] flex items-center justify-center px-4 py-8">
|
<div className="min-h-[calc(100vh-4rem)] flex items-center justify-center px-4 py-8 relative overflow-hidden">
|
||||||
<motion.div
|
{/* Background effects */}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<GlowOrbs />
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<div className="absolute inset-0 cyber-grid opacity-20" />
|
||||||
transition={{ duration: 0.4 }}
|
|
||||||
className="w-full max-w-md"
|
{/* Decorative elements */}
|
||||||
>
|
<div className="absolute top-20 left-10 hidden lg:block">
|
||||||
<Card className="shadow-lg">
|
<motion.div
|
||||||
<CardHeader className="space-y-1 text-center pb-4">
|
animate={{ y: [0, -10, 0] }}
|
||||||
<motion.div
|
transition={{ duration: 3, repeat: Infinity }}
|
||||||
initial={{ scale: 0 }}
|
className="text-[var(--color-neon-cyan)]/20 font-mono text-xs"
|
||||||
animate={{ scale: 1 }}
|
>
|
||||||
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
|
{"// initializing new user..."}
|
||||||
className="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center"
|
</motion.div>
|
||||||
>
|
</div>
|
||||||
<Trophy className="h-8 w-8 text-primary" />
|
<div className="absolute bottom-20 right-10 hidden lg:block">
|
||||||
</motion.div>
|
<motion.div
|
||||||
<CardTitle className="text-2xl font-bold">Регистрация</CardTitle>
|
animate={{ opacity: [0.2, 0.5, 0.2] }}
|
||||||
<p className="text-sm text-muted-foreground">
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
Создайте аккаунт для участия в соревнованиях
|
className="text-[var(--color-neon-purple)]/30 font-mono text-xs"
|
||||||
|
>
|
||||||
|
{"<create_account/>"}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-5xl grid lg:grid-cols-2 gap-8 items-center relative z-10">
|
||||||
|
{/* Left side - Illustration (hidden on mobile) */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -30 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="hidden lg:block"
|
||||||
|
>
|
||||||
|
<AuthIllustration type="register" className="w-full max-w-md mx-auto" />
|
||||||
|
<div className="text-center mt-8">
|
||||||
|
<p className="text-muted-foreground font-mono text-sm">
|
||||||
|
<span className="text-[var(--color-neon-cyan)]">$</span> Создание нового аккаунта
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent>
|
</motion.div>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
{error && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: "auto" }}
|
|
||||||
>
|
|
||||||
<AlertError>{error}</AlertError>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
{/* Right side - Register form */}
|
||||||
<Label htmlFor="email">Email</Label>
|
<motion.div
|
||||||
<div className="relative">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<Input
|
transition={{ duration: 0.4 }}
|
||||||
id="email"
|
className="w-full max-w-md mx-auto"
|
||||||
type="email"
|
>
|
||||||
value={email}
|
<Card variant="cyber" className="relative overflow-hidden">
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
{/* Card glow effect */}
|
||||||
required
|
<div className="absolute inset-0 bg-gradient-to-br from-[var(--color-neon-cyan)]/5 via-transparent to-[var(--color-neon-purple)]/5" />
|
||||||
className="pl-10"
|
|
||||||
placeholder="email@example.com"
|
<CardHeader className="space-y-1 text-center pb-4 relative">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
|
||||||
|
className="relative mx-auto mb-4"
|
||||||
|
>
|
||||||
|
<div className="w-16 h-16 rounded-full border-2 border-[var(--color-neon-cyan)] flex items-center justify-center relative">
|
||||||
|
<VolguLogo className="h-8 w-8" animated />
|
||||||
|
{/* Animated ring */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 rounded-full border border-[var(--color-neon-cyan)]/50"
|
||||||
|
animate={{ scale: [1, 1.2, 1], opacity: [0.5, 0, 0.5] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<CardTitle className="text-2xl font-display font-bold">
|
||||||
<Label htmlFor="username">Имя пользователя</Label>
|
<GlitchText text="РЕГИСТРАЦИЯ" intensity="low" color="var(--color-neon-cyan)" />
|
||||||
<div className="relative">
|
</CardTitle>
|
||||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<p className="text-sm text-muted-foreground font-mono">
|
||||||
<Input
|
<span className="text-[var(--color-neon-purple)]">></span> Создайте аккаунт для участия
|
||||||
id="username"
|
|
||||||
type="text"
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
required
|
|
||||||
className="pl-10"
|
|
||||||
placeholder="username"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="password">Пароль</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
className="pl-10 pr-10"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<EyeOff className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="confirmPassword">Подтвердите пароль</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
id="confirmPassword"
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
className="pl-10"
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Password requirements */}
|
|
||||||
{password.length > 0 && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, height: 0 }}
|
|
||||||
animate={{ opacity: 1, height: "auto" }}
|
|
||||||
className="space-y-1"
|
|
||||||
>
|
|
||||||
<div className={`flex items-center gap-2 text-xs ${passwordChecks.length ? "text-success" : "text-muted-foreground"}`}>
|
|
||||||
{passwordChecks.length ? <Check className="h-3 w-3" /> : <X className="h-3 w-3" />}
|
|
||||||
Минимум 6 символов
|
|
||||||
</div>
|
|
||||||
{confirmPassword.length > 0 && (
|
|
||||||
<div className={`flex items-center gap-2 text-xs ${passwordChecks.match ? "text-success" : "text-destructive"}`}>
|
|
||||||
{passwordChecks.match ? <Check className="h-3 w-3" /> : <X className="h-3 w-3" />}
|
|
||||||
Пароли совпадают
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full" loading={isLoading}>
|
|
||||||
<UserPlus className="h-4 w-4 mr-2" />
|
|
||||||
Зарегистрироваться
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Уже есть аккаунт?{" "}
|
|
||||||
<Link
|
|
||||||
href="/login"
|
|
||||||
className="text-primary hover:underline font-medium"
|
|
||||||
>
|
|
||||||
Войти
|
|
||||||
</Link>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</CardHeader>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
<CardContent className="relative">
|
||||||
</motion.div>
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
|
>
|
||||||
|
<AlertError>{error}</AlertError>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<div className="relative group">
|
||||||
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-[var(--color-neon-cyan)] transition-colors" />
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
className="pl-10 font-mono"
|
||||||
|
placeholder="user@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="username" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
|
Имя пользователя
|
||||||
|
</Label>
|
||||||
|
<div className="relative group">
|
||||||
|
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-[var(--color-neon-cyan)] transition-colors" />
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
className="pl-10 font-mono"
|
||||||
|
placeholder="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
|
Пароль
|
||||||
|
</Label>
|
||||||
|
<div className="relative group">
|
||||||
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-[var(--color-neon-cyan)] transition-colors" />
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="pl-10 pr-10 font-mono"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-[var(--color-neon-cyan)] transition-colors"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="confirmPassword" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
|
Подтвердите пароль
|
||||||
|
</Label>
|
||||||
|
<div className="relative group">
|
||||||
|
<Shield className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-[var(--color-neon-cyan)] transition-colors" />
|
||||||
|
<Input
|
||||||
|
id="confirmPassword"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="pl-10 font-mono"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password requirements */}
|
||||||
|
{password.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
|
className="space-y-2 p-3 rounded-lg bg-[var(--color-secondary)]/50 border border-[var(--color-border)]"
|
||||||
|
>
|
||||||
|
<div className={`flex items-center gap-2 text-xs font-mono ${passwordChecks.length ? "text-[var(--color-neon-green)]" : "text-muted-foreground"}`}>
|
||||||
|
{passwordChecks.length ? (
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
<span>{passwordChecks.length ? "[OK]" : "[--]"}</span>
|
||||||
|
Минимум 6 символов
|
||||||
|
</div>
|
||||||
|
{confirmPassword.length > 0 && (
|
||||||
|
<div className={`flex items-center gap-2 text-xs font-mono ${passwordChecks.match ? "text-[var(--color-neon-green)]" : "text-[var(--color-destructive)]"}`}>
|
||||||
|
{passwordChecks.match ? (
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
<span>{passwordChecks.match ? "[OK]" : "[ERR]"}</span>
|
||||||
|
Пароли совпадают
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button type="submit" variant="neon" className="w-full group" loading={isLoading}>
|
||||||
|
<UserPlus className="h-4 w-4 mr-2" />
|
||||||
|
Создать аккаунт
|
||||||
|
<ArrowRight className="h-4 w-4 ml-2 transition-transform group-hover:translate-x-1" />
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="relative my-6">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-[var(--color-border)]" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-xs">
|
||||||
|
<span className="px-2 bg-[var(--color-card)] text-muted-foreground font-mono">
|
||||||
|
или
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-muted-foreground font-mono">
|
||||||
|
Уже есть аккаунт?{" "}
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="text-[var(--color-neon-green)] hover:text-[var(--color-neon-cyan)] transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Войти
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom decoration */}
|
||||||
|
<div className="mt-6 pt-4 border-t border-[var(--color-border)]">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Terminal className="h-4 w-4 text-[var(--color-neon-cyan)]" />
|
||||||
|
<CyberBrandText className="text-xs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { AlertError } from "@/components/ui/alert";
|
import { AlertError } from "@/components/ui/alert";
|
||||||
import { Progress } from "@/components/ui/progress";
|
|
||||||
import { SubmissionStatus } from "@/components/domain/submission-status";
|
import { SubmissionStatus } from "@/components/domain/submission-status";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@ -29,6 +28,8 @@ import {
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
Filter,
|
Filter,
|
||||||
Hash,
|
Hash,
|
||||||
|
Activity,
|
||||||
|
Cpu,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@ -37,6 +38,8 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { GlowOrbs, GlitchText } from "@/components/decorative";
|
||||||
|
import { EmptyStateIllustration } from "@/components/illustrations";
|
||||||
import type { SubmissionListItem } from "@/types";
|
import type { SubmissionListItem } from "@/types";
|
||||||
|
|
||||||
// Stats card component
|
// Stats card component
|
||||||
@ -45,20 +48,45 @@ function StatsCard({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
color,
|
color,
|
||||||
|
glowColor,
|
||||||
}: {
|
}: {
|
||||||
icon: typeof Trophy;
|
icon: typeof Trophy;
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
glowColor?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card variant="cyber" className="relative overflow-hidden group">
|
||||||
|
{/* Top accent */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 right-0 h-[2px] opacity-60"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(90deg, transparent, ${glowColor || "var(--color-neon-green)"}, transparent)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Icon className={`h-5 w-5 ${color || "text-muted-foreground"}`} />
|
<div
|
||||||
|
className="w-10 h-10 rounded-lg flex items-center justify-center transition-colors"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `color-mix(in srgb, ${glowColor || "var(--color-neon-green)"} 15%, transparent)`,
|
||||||
|
border: `1px solid ${glowColor || "var(--color-neon-green)"}40`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5" style={{ color: glowColor || color }} />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted-foreground">{label}</div>
|
<div className="text-xs text-muted-foreground font-mono uppercase tracking-wider">{label}</div>
|
||||||
<div className="font-semibold text-lg">{value}</div>
|
<div
|
||||||
|
className="font-display font-bold text-xl"
|
||||||
|
style={{
|
||||||
|
color: glowColor || color,
|
||||||
|
textShadow: glowColor ? `0 0 10px ${glowColor}` : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -116,10 +144,13 @@ export default function SubmissionsPage() {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<Skeleton className="h-10 w-48 mb-8" />
|
<div className="flex items-center gap-4 mb-8">
|
||||||
|
<Skeleton className="h-12 w-12 rounded-lg" />
|
||||||
|
<Skeleton className="h-10 w-48" />
|
||||||
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-4 mb-8">
|
<div className="grid gap-4 md:grid-cols-4 mb-8">
|
||||||
{[...Array(4)].map((_, i) => (
|
{[...Array(4)].map((_, i) => (
|
||||||
<Skeleton key={i} className="h-20 rounded-xl" />
|
<Skeleton key={i} className="h-24 rounded-xl" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="h-64 rounded-xl" />
|
<Skeleton className="h-64 rounded-xl" />
|
||||||
@ -136,219 +167,271 @@ export default function SubmissionsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="min-h-[calc(100vh-4rem)] relative">
|
||||||
{/* Header */}
|
{/* Background */}
|
||||||
<motion.div
|
<GlowOrbs />
|
||||||
initial={{ opacity: 0, y: -20 }}
|
<div className="absolute inset-0 cyber-grid opacity-10" />
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="mb-8"
|
|
||||||
>
|
|
||||||
<h1 className="text-3xl font-bold flex items-center gap-3">
|
|
||||||
<FileCode className="h-8 w-8 text-primary" />
|
|
||||||
Мои решения
|
|
||||||
</h1>
|
|
||||||
<p className="text-muted-foreground mt-1">
|
|
||||||
История всех отправленных решений
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Stats */}
|
<div className="container mx-auto px-4 py-8 relative">
|
||||||
<motion.div
|
{/* Header */}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.1 }}
|
|
||||||
className="grid gap-4 md:grid-cols-4 mb-8"
|
|
||||||
>
|
|
||||||
<StatsCard icon={Hash} label="Всего отправок" value={stats.total} />
|
|
||||||
<StatsCard
|
|
||||||
icon={CheckCircle2}
|
|
||||||
label="Принято"
|
|
||||||
value={stats.accepted}
|
|
||||||
color="text-success"
|
|
||||||
/>
|
|
||||||
<StatsCard
|
|
||||||
icon={Trophy}
|
|
||||||
label="Частично"
|
|
||||||
value={stats.partial}
|
|
||||||
color="text-warning"
|
|
||||||
/>
|
|
||||||
<StatsCard
|
|
||||||
icon={XCircle}
|
|
||||||
label="Не принято"
|
|
||||||
value={stats.failed}
|
|
||||||
color="text-destructive"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Success rate */}
|
|
||||||
{submissions.length > 0 && (
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.2 }}
|
|
||||||
className="mb-8"
|
className="mb-8"
|
||||||
>
|
>
|
||||||
<Card>
|
<div className="flex items-center gap-4">
|
||||||
<CardContent className="pt-4">
|
<div className="w-12 h-12 rounded-lg bg-[var(--color-neon-purple)]/10 border border-[var(--color-neon-purple)]/30 flex items-center justify-center">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<FileCode className="h-6 w-6 text-[var(--color-neon-purple)]" />
|
||||||
<span className="text-sm text-muted-foreground">
|
</div>
|
||||||
Процент успешных решений
|
<div>
|
||||||
</span>
|
<h1 className="text-3xl font-display font-bold">
|
||||||
<span className="font-semibold">{stats.successRate}%</span>
|
<GlitchText text="МОИ РЕШЕНИЯ" intensity="low" color="var(--color-neon-purple)" />
|
||||||
</div>
|
</h1>
|
||||||
<Progress value={stats.successRate} className="h-2" />
|
<p className="text-sm font-mono text-muted-foreground">
|
||||||
</CardContent>
|
<span className="text-[var(--color-neon-cyan)]">$</span> submissions.history()
|
||||||
</Card>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
{submissions.length === 0 ? (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
>
|
|
||||||
<Card className="text-center py-16">
|
|
||||||
<CardContent>
|
|
||||||
<FileCode className="h-16 w-16 mx-auto mb-4 text-muted-foreground opacity-50" />
|
|
||||||
<h2 className="text-xl font-semibold mb-2">
|
|
||||||
У вас пока нет решений
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground mb-6">
|
|
||||||
Отправьте своё первое решение, чтобы увидеть его здесь
|
|
||||||
</p>
|
</p>
|
||||||
<Button asChild>
|
</div>
|
||||||
<Link href="/contests">
|
</div>
|
||||||
Перейти к контестам
|
|
||||||
<ArrowRight className="h-4 w-4 ml-2" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
|
||||||
|
{/* Stats */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.3 }}
|
transition={{ delay: 0.1 }}
|
||||||
|
className="grid gap-4 md:grid-cols-4 mb-8"
|
||||||
>
|
>
|
||||||
<Card>
|
<StatsCard
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
icon={Hash}
|
||||||
<CardTitle className="text-lg">
|
label="Всего отправок"
|
||||||
История отправок ({filteredSubmissions.length})
|
value={stats.total}
|
||||||
</CardTitle>
|
glowColor="var(--color-neon-cyan)"
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
/>
|
||||||
<SelectTrigger className="w-40">
|
<StatsCard
|
||||||
<Filter className="h-4 w-4 mr-2" />
|
icon={CheckCircle2}
|
||||||
<SelectValue />
|
label="Принято"
|
||||||
</SelectTrigger>
|
value={stats.accepted}
|
||||||
<SelectContent>
|
glowColor="var(--color-neon-green)"
|
||||||
<SelectItem value="all">Все статусы</SelectItem>
|
/>
|
||||||
<SelectItem value="accepted">Принятые</SelectItem>
|
<StatsCard
|
||||||
<SelectItem value="partial">Частичные</SelectItem>
|
icon={Trophy}
|
||||||
<SelectItem value="failed">Не принятые</SelectItem>
|
label="Частично"
|
||||||
</SelectContent>
|
value={stats.partial}
|
||||||
</Select>
|
glowColor="var(--color-neon-yellow)"
|
||||||
</CardHeader>
|
/>
|
||||||
<CardContent>
|
<StatsCard
|
||||||
<Table>
|
icon={XCircle}
|
||||||
<TableHeader>
|
label="Не принято"
|
||||||
<TableRow>
|
value={stats.failed}
|
||||||
<TableHead className="w-20">ID</TableHead>
|
glowColor="var(--color-destructive)"
|
||||||
<TableHead>Время</TableHead>
|
/>
|
||||||
<TableHead>Язык</TableHead>
|
|
||||||
<TableHead className="text-center">Статус</TableHead>
|
|
||||||
<TableHead className="text-right">Баллы</TableHead>
|
|
||||||
<TableHead className="text-right">Тесты</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<AnimatePresence>
|
|
||||||
{filteredSubmissions.map((submission, index) => {
|
|
||||||
const scorePercent =
|
|
||||||
submission.total_points > 0
|
|
||||||
? Math.round(
|
|
||||||
(submission.score / submission.total_points) * 100
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.tr
|
|
||||||
key={submission.id}
|
|
||||||
initial={{ opacity: 0, x: -20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: 20 }}
|
|
||||||
transition={{ delay: index * 0.03 }}
|
|
||||||
className="border-b border-border hover:bg-muted/50"
|
|
||||||
>
|
|
||||||
<TableCell className="font-mono text-muted-foreground">
|
|
||||||
#{submission.id}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span className="text-sm">
|
|
||||||
{formatDate(submission.created_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant="outline">
|
|
||||||
{submission.language_name || "-"}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
<SubmissionStatus status={submission.status} />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<span
|
|
||||||
className={`font-semibold ${
|
|
||||||
scorePercent === 100
|
|
||||||
? "text-success"
|
|
||||||
: scorePercent > 0
|
|
||||||
? "text-warning"
|
|
||||||
: "text-muted-foreground"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{submission.score}/{submission.total_points}
|
|
||||||
</span>
|
|
||||||
{scorePercent > 0 && scorePercent < 100 && (
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
({scorePercent}%)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
submission.tests_passed === submission.tests_total
|
|
||||||
? "success"
|
|
||||||
: submission.tests_passed > 0
|
|
||||||
? "warning"
|
|
||||||
: "secondary"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{submission.tests_passed}/{submission.tests_total}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
</motion.tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</AnimatePresence>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
{filteredSubmissions.length === 0 && (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
Нет решений с выбранным статусом
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
|
||||||
|
{/* Success rate */}
|
||||||
|
{submissions.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<Card variant="cyber" className="relative overflow-hidden">
|
||||||
|
{/* Background decoration */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-[var(--color-neon-green)]/5 via-transparent to-[var(--color-neon-cyan)]/5" />
|
||||||
|
|
||||||
|
<CardContent className="pt-4 relative">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Activity className="h-4 w-4 text-[var(--color-neon-green)]" />
|
||||||
|
<span className="text-sm font-mono text-muted-foreground uppercase tracking-wider">
|
||||||
|
SUCCESS_RATE
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="font-display font-bold text-xl"
|
||||||
|
style={{
|
||||||
|
color: stats.successRate >= 70 ? "var(--color-neon-green)" : stats.successRate >= 40 ? "var(--color-neon-yellow)" : "var(--color-neon-orange)",
|
||||||
|
textShadow: `0 0 10px ${stats.successRate >= 70 ? "var(--color-neon-green)" : stats.successRate >= 40 ? "var(--color-neon-yellow)" : "var(--color-neon-orange)"}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stats.successRate}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative h-3 bg-[var(--color-secondary)] rounded-full overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-y-0 left-0 rounded-full"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(90deg, var(--color-neon-green), var(--color-neon-cyan))`,
|
||||||
|
boxShadow: "0 0 15px var(--color-neon-green)",
|
||||||
|
}}
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${stats.successRate}%` }}
|
||||||
|
transition={{ duration: 1, ease: "easeOut", delay: 0.3 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{submissions.length === 0 ? (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="flex flex-col items-center justify-center py-16"
|
||||||
|
>
|
||||||
|
<EmptyStateIllustration type="no-submissions" />
|
||||||
|
<h2 className="text-xl font-display font-semibold mt-4 mb-2">
|
||||||
|
<span className="text-[var(--color-neon-purple)]">></span> Нет решений
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground font-mono text-sm mb-6">
|
||||||
|
// отправьте своё первое решение
|
||||||
|
</p>
|
||||||
|
<Button asChild variant="cyber">
|
||||||
|
<Link href="/contests">
|
||||||
|
Перейти к контестам
|
||||||
|
<ArrowRight className="h-4 w-4 ml-2" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
>
|
||||||
|
<Card variant="cyber" className="relative overflow-hidden">
|
||||||
|
{/* Top accent */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-[var(--color-neon-purple)]/50 to-transparent" />
|
||||||
|
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Cpu className="h-5 w-5 text-[var(--color-neon-purple)]" />
|
||||||
|
<CardTitle className="text-lg font-display">
|
||||||
|
ИСТОРИЯ ОТПРАВОК
|
||||||
|
<Badge variant="purple" className="ml-2 font-mono">
|
||||||
|
{filteredSubmissions.length}
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</div>
|
||||||
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
|
<SelectTrigger className="w-44 font-mono">
|
||||||
|
<Filter className="h-4 w-4 mr-2 text-[var(--color-neon-cyan)]" />
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all" className="font-mono">Все статусы</SelectItem>
|
||||||
|
<SelectItem value="accepted" className="font-mono">Принятые</SelectItem>
|
||||||
|
<SelectItem value="partial" className="font-mono">Частичные</SelectItem>
|
||||||
|
<SelectItem value="failed" className="font-mono">Не принятые</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="border-[var(--color-border)]">
|
||||||
|
<TableHead className="w-20 font-mono text-[var(--color-neon-cyan)]">ID</TableHead>
|
||||||
|
<TableHead className="font-mono text-[var(--color-neon-cyan)]">Время</TableHead>
|
||||||
|
<TableHead className="font-mono text-[var(--color-neon-cyan)]">Язык</TableHead>
|
||||||
|
<TableHead className="text-center font-mono text-[var(--color-neon-cyan)]">Статус</TableHead>
|
||||||
|
<TableHead className="text-right font-mono text-[var(--color-neon-cyan)]">Баллы</TableHead>
|
||||||
|
<TableHead className="text-right font-mono text-[var(--color-neon-cyan)]">Тесты</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<AnimatePresence>
|
||||||
|
{filteredSubmissions.map((submission, index) => {
|
||||||
|
const scorePercent =
|
||||||
|
submission.total_points > 0
|
||||||
|
? Math.round(
|
||||||
|
(submission.score / submission.total_points) * 100
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.tr
|
||||||
|
key={submission.id}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: 20 }}
|
||||||
|
transition={{ delay: index * 0.03 }}
|
||||||
|
className="border-b border-[var(--color-border)] hover:bg-[var(--color-neon-green)]/5 transition-colors"
|
||||||
|
>
|
||||||
|
<TableCell className="font-mono text-muted-foreground">
|
||||||
|
<span className="text-[var(--color-neon-purple)]">#</span>{submission.id}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2 font-mono text-sm">
|
||||||
|
<Clock className="h-4 w-4 text-[var(--color-neon-cyan)]" />
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{formatDate(submission.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="outline" className="font-mono">
|
||||||
|
{submission.language_name || "-"}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<SubmissionStatus status={submission.status} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex items-center justify-end gap-2 font-mono">
|
||||||
|
<span
|
||||||
|
className="font-semibold"
|
||||||
|
style={{
|
||||||
|
color: scorePercent === 100
|
||||||
|
? "var(--color-neon-green)"
|
||||||
|
: scorePercent > 0
|
||||||
|
? "var(--color-neon-yellow)"
|
||||||
|
: "var(--color-muted-foreground)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{submission.score}/{submission.total_points}
|
||||||
|
</span>
|
||||||
|
{scorePercent > 0 && scorePercent < 100 && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
({scorePercent}%)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
submission.tests_passed === submission.tests_total
|
||||||
|
? "success"
|
||||||
|
: submission.tests_passed > 0
|
||||||
|
? "warning"
|
||||||
|
: "secondary"
|
||||||
|
}
|
||||||
|
className="font-mono"
|
||||||
|
>
|
||||||
|
{submission.tests_passed}/{submission.tests_total}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
</motion.tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</AnimatePresence>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{filteredSubmissions.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-muted-foreground font-mono">
|
||||||
|
<span className="text-[var(--color-neon-cyan)]">></span> Нет решений с выбранным статусом
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,9 +21,10 @@ import {
|
|||||||
User,
|
User,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
|
Terminal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { VolguLogo } from "@/components/VolguLogo";
|
import { VolguLogo, CyberBrandText } from "@/components/VolguLogo";
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||||||
|
|
||||||
@ -42,22 +43,26 @@ export function Navbar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<nav className="sticky top-0 z-50 border-b border-[var(--color-border)] bg-[var(--color-background)]/90 backdrop-blur-md">
|
||||||
|
{/* Neon bottom border */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-[1px] bg-gradient-to-r from-transparent via-[var(--color-neon-green)]/50 to-transparent" />
|
||||||
|
|
||||||
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
|
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
|
||||||
{/* Logo and Nav Links */}
|
{/* Logo and Nav Links */}
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<Link href="/" className="flex items-center gap-2 group">
|
<Link href="/" className="flex items-center gap-3 group">
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
|
className="relative"
|
||||||
>
|
>
|
||||||
<VolguLogo className="h-8 w-8" />
|
<VolguLogo className="h-8 w-8" />
|
||||||
|
{/* Glow effect on hover */}
|
||||||
|
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-md">
|
||||||
|
<VolguLogo className="h-8 w-8" />
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<span className="text-xl font-bold">
|
<CyberBrandText />
|
||||||
<span className="text-[#2B4B7C]">ВолГУ</span>
|
|
||||||
<span className="text-muted-foreground">.</span>
|
|
||||||
<span className="text-primary">Контесты</span>
|
|
||||||
</span>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
@ -71,14 +76,24 @@ export function Navbar() {
|
|||||||
key={link.href}
|
key={link.href}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors",
|
"relative flex items-center gap-2 px-4 py-2 text-sm font-mono uppercase tracking-wider transition-all duration-200",
|
||||||
active
|
active
|
||||||
? "bg-primary/10 text-primary"
|
? "text-[var(--color-neon-green)]"
|
||||||
: "text-muted-foreground hover:text-foreground hover:bg-muted"
|
: "text-muted-foreground hover:text-[var(--color-neon-green)]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
{link.label}
|
{link.label}
|
||||||
|
{/* Neon underline for active state */}
|
||||||
|
{active && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="navbar-indicator"
|
||||||
|
className="absolute bottom-0 left-0 right-0 h-[2px] bg-[var(--color-neon-green)]"
|
||||||
|
style={{
|
||||||
|
boxShadow: "0 0 10px var(--color-neon-green)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -87,14 +102,23 @@ export function Navbar() {
|
|||||||
<Link
|
<Link
|
||||||
href="/admin"
|
href="/admin"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors",
|
"relative flex items-center gap-2 px-4 py-2 text-sm font-mono uppercase tracking-wider transition-all duration-200",
|
||||||
isActive("/admin")
|
isActive("/admin")
|
||||||
? "bg-primary/10 text-primary"
|
? "text-[var(--color-neon-purple)]"
|
||||||
: "text-muted-foreground hover:text-foreground hover:bg-muted"
|
: "text-muted-foreground hover:text-[var(--color-neon-purple)]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<LayoutDashboard className="h-4 w-4" />
|
<LayoutDashboard className="h-4 w-4" />
|
||||||
Админ
|
Админ
|
||||||
|
{isActive("/admin") && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="navbar-indicator-admin"
|
||||||
|
className="absolute bottom-0 left-0 right-0 h-[2px] bg-[var(--color-neon-purple)]"
|
||||||
|
style={{
|
||||||
|
boxShadow: "0 0 10px var(--color-neon-purple)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -111,42 +135,57 @@ export function Navbar() {
|
|||||||
) : user ? (
|
) : user ? (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="flex items-center gap-2 px-2">
|
<Button
|
||||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-primary to-primary/60 flex items-center justify-center text-primary-foreground font-semibold text-sm overflow-hidden">
|
variant="ghost"
|
||||||
{user.avatar_url ? (
|
className="flex items-center gap-2 px-2 border border-transparent hover:border-[var(--color-neon-green)]/30"
|
||||||
<img
|
>
|
||||||
src={`${API_URL}${user.avatar_url}`}
|
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-[var(--color-neon-green)] to-[var(--color-neon-cyan)] p-[2px] overflow-hidden">
|
||||||
alt={user.username}
|
<div className="w-full h-full rounded-full bg-[var(--color-background)] flex items-center justify-center text-[var(--color-neon-green)] font-mono text-sm">
|
||||||
className="w-full h-full object-cover"
|
{user.avatar_url ? (
|
||||||
/>
|
<img
|
||||||
) : (
|
src={`${API_URL}${user.avatar_url}`}
|
||||||
user.username.charAt(0).toUpperCase()
|
alt={user.username}
|
||||||
)}
|
className="w-full h-full object-cover rounded-full"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
user.username.charAt(0).toUpperCase()
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="hidden sm:inline text-sm font-medium">
|
<span className="hidden sm:inline text-sm font-mono text-foreground">
|
||||||
{user.username}
|
{user.username}
|
||||||
</span>
|
</span>
|
||||||
{user.role === "admin" && (
|
{user.role === "admin" && (
|
||||||
<Badge variant="secondary" className="hidden sm:inline-flex text-xs">
|
<Badge variant="purple" className="hidden sm:inline-flex">
|
||||||
Admin
|
ADMIN
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-48">
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="w-48 bg-[var(--color-card)] border-[var(--color-border)]"
|
||||||
|
>
|
||||||
<div className="px-2 py-1.5">
|
<div className="px-2 py-1.5">
|
||||||
<p className="text-sm font-medium">{user.username}</p>
|
<p className="text-sm font-mono text-[var(--color-neon-green)]">
|
||||||
<p className="text-xs text-muted-foreground">{user.email}</p>
|
{user.username}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground font-mono">
|
||||||
|
{user.email}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator className="bg-[var(--color-border)]" />
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/profile" className="flex items-center gap-2">
|
<Link
|
||||||
|
href="/profile"
|
||||||
|
className="flex items-center gap-2 font-mono text-sm"
|
||||||
|
>
|
||||||
<User className="h-4 w-4" />
|
<User className="h-4 w-4" />
|
||||||
Профиль
|
Профиль
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator className="bg-[var(--color-border)]" />
|
||||||
|
|
||||||
{/* Mobile nav links */}
|
{/* Mobile nav links */}
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
@ -154,7 +193,10 @@ export function Navbar() {
|
|||||||
const Icon = link.icon;
|
const Icon = link.icon;
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem key={link.href} asChild>
|
<DropdownMenuItem key={link.href} asChild>
|
||||||
<Link href={link.href} className="flex items-center gap-2">
|
<Link
|
||||||
|
href={link.href}
|
||||||
|
className="flex items-center gap-2 font-mono text-sm"
|
||||||
|
>
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
{link.label}
|
{link.label}
|
||||||
</Link>
|
</Link>
|
||||||
@ -163,18 +205,21 @@ export function Navbar() {
|
|||||||
})}
|
})}
|
||||||
{user.role === "admin" && (
|
{user.role === "admin" && (
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/admin" className="flex items-center gap-2">
|
<Link
|
||||||
|
href="/admin"
|
||||||
|
className="flex items-center gap-2 font-mono text-sm"
|
||||||
|
>
|
||||||
<LayoutDashboard className="h-4 w-4" />
|
<LayoutDashboard className="h-4 w-4" />
|
||||||
Админ
|
Админ
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator className="bg-[var(--color-border)]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-[var(--color-destructive)] focus:text-[var(--color-destructive)] font-mono text-sm"
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4 mr-2" />
|
<LogOut className="h-4 w-4 mr-2" />
|
||||||
Выйти
|
Выйти
|
||||||
@ -182,11 +227,14 @@ export function Navbar() {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<Button variant="ghost" asChild>
|
<Button variant="ghost" asChild className="font-mono">
|
||||||
<Link href="/login">Войти</Link>
|
<Link href="/login">
|
||||||
|
<Terminal className="h-4 w-4 mr-2" />
|
||||||
|
Войти
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild>
|
<Button asChild className="font-mono">
|
||||||
<Link href="/register">Регистрация</Link>
|
<Link href="/register">Регистрация</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,11 +1,58 @@
|
|||||||
export function VolguLogo({ className = "h-8 w-8" }: { className?: string }) {
|
"use client";
|
||||||
return (
|
|
||||||
|
import React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface VolguLogoProps {
|
||||||
|
className?: string;
|
||||||
|
animated?: boolean;
|
||||||
|
variant?: "default" | "cyber" | "minimal";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VolguLogo({
|
||||||
|
className = "h-8 w-8",
|
||||||
|
animated = false,
|
||||||
|
variant = "cyber",
|
||||||
|
}: VolguLogoProps) {
|
||||||
|
if (variant === "minimal") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"font-display font-bold text-[var(--color-neon-green)]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
V
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogoSvg = (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 198 209"
|
viewBox="0 0 198 209"
|
||||||
className={className}
|
className={className}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<g transform="translate(0,209) scale(0.1,-0.1)" fill="#2B4B7C" stroke="none">
|
<defs>
|
||||||
|
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor="var(--color-neon-green)" />
|
||||||
|
<stop offset="100%" stopColor="var(--color-neon-cyan)" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="logoGlow">
|
||||||
|
<feGaussianBlur stdDeviation="2" result="coloredBlur" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
transform="translate(0,209) scale(0.1,-0.1)"
|
||||||
|
fill={variant === "cyber" ? "url(#logoGradient)" : "var(--color-neon-green)"}
|
||||||
|
stroke="none"
|
||||||
|
filter={variant === "cyber" ? "url(#logoGlow)" : undefined}
|
||||||
|
>
|
||||||
<path d="M1940 2084 c-8 -2 -433 -51 -945 -109 -512 -58 -945 -108 -963 -111
|
<path d="M1940 2084 c-8 -2 -433 -51 -945 -109 -512 -58 -945 -108 -963 -111
|
||||||
-26 -4 -32 -10 -32 -29 0 -22 5 -25 35 -25 55 0 140 -37 193 -85 61 -55 99
|
-26 -4 -32 -10 -32 -29 0 -22 5 -25 35 -25 55 0 140 -37 193 -85 61 -55 99
|
||||||
-118 245 -412 144 -288 171 -317 102 -113 -38 113 -44 182 -19 218 19 27 39
|
-118 245 -412 144 -288 171 -317 102 -113 -38 113 -44 182 -19 218 19 27 39
|
||||||
@ -16,8 +63,39 @@ export function VolguLogo({ className = "h-8 w-8" }: { className?: string }) {
|
|||||||
-94 -33 -129 -55 -110 -67 8 -5 22 -9 31 -9 21 0 41 -35 34 -58 -8 -24 -54
|
-94 -33 -129 -55 -110 -67 8 -5 22 -9 31 -9 21 0 41 -35 34 -58 -8 -24 -54
|
||||||
-50 -105 -58 -38 -6 -40 -8 -26 -24 8 -9 15 -26 15 -37 0 -26 -54 -73 -82 -73
|
-50 -105 -58 -38 -6 -40 -8 -26 -24 8 -9 15 -26 15 -37 0 -26 -54 -73 -82 -73
|
||||||
-15 0 -19 -5 -15 -19 2 -11 65 -164 138 -340 l134 -321 30 0 c29 0 32 4 97
|
-15 0 -19 -5 -15 -19 2 -11 65 -164 138 -340 l134 -321 30 0 c29 0 32 4 97
|
||||||
158 37 86 236 551 443 1032 206 481 375 880 375 887 0 13 -10 14 -40 7z"/>
|
158 37 86 236 551 443 1032 206 481 375 880 375 887 0 13 -10 14 -40 7z" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (animated) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
filter: [
|
||||||
|
"drop-shadow(0 0 5px var(--color-neon-green))",
|
||||||
|
"drop-shadow(0 0 15px var(--color-neon-green))",
|
||||||
|
"drop-shadow(0 0 5px var(--color-neon-green))",
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
|
>
|
||||||
|
{LogoSvg}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LogoSvg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brand text component for the navbar
|
||||||
|
export function CyberBrandText({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<span className={cn("font-mono text-xl tracking-wider", className)}>
|
||||||
|
<span className="text-[var(--color-neon-green)] text-glow-green">></span>
|
||||||
|
<span className="text-[var(--color-neon-cyan)]">VOLGU</span>
|
||||||
|
<span className="text-muted-foreground">.</span>
|
||||||
|
<span className="text-[var(--color-neon-green)]">CONTESTS</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
78
frontend/src/components/decorative/CornerBrackets.tsx
Normal file
78
frontend/src/components/decorative/CornerBrackets.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface CornerBracketsProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
color?: "green" | "cyan" | "purple";
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorStyles = {
|
||||||
|
green: "border-[var(--color-neon-green)]",
|
||||||
|
cyan: "border-[var(--color-neon-cyan)]",
|
||||||
|
purple: "border-[var(--color-neon-purple)]",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeStyles = {
|
||||||
|
sm: { bracket: "w-3 h-3", offset: "4px" },
|
||||||
|
md: { bracket: "w-4 h-4", offset: "8px" },
|
||||||
|
lg: { bracket: "w-6 h-6", offset: "12px" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CornerBrackets({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
color = "green",
|
||||||
|
size = "md",
|
||||||
|
}: CornerBracketsProps) {
|
||||||
|
const { bracket, offset } = sizeStyles[size];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative", className)}>
|
||||||
|
{/* Top Left */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute border-t-2 border-l-2 transition-opacity",
|
||||||
|
bracket,
|
||||||
|
colorStyles[color],
|
||||||
|
"opacity-50 group-hover:opacity-100"
|
||||||
|
)}
|
||||||
|
style={{ top: offset, left: offset }}
|
||||||
|
/>
|
||||||
|
{/* Top Right */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute border-t-2 border-r-2 transition-opacity",
|
||||||
|
bracket,
|
||||||
|
colorStyles[color],
|
||||||
|
"opacity-50 group-hover:opacity-100"
|
||||||
|
)}
|
||||||
|
style={{ top: offset, right: offset }}
|
||||||
|
/>
|
||||||
|
{/* Bottom Left */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute border-b-2 border-l-2 transition-opacity",
|
||||||
|
bracket,
|
||||||
|
colorStyles[color],
|
||||||
|
"opacity-50 group-hover:opacity-100"
|
||||||
|
)}
|
||||||
|
style={{ bottom: offset, left: offset }}
|
||||||
|
/>
|
||||||
|
{/* Bottom Right */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute border-b-2 border-r-2 transition-opacity",
|
||||||
|
bracket,
|
||||||
|
colorStyles[color],
|
||||||
|
"opacity-50 group-hover:opacity-100"
|
||||||
|
)}
|
||||||
|
style={{ bottom: offset, right: offset }}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
frontend/src/components/decorative/GlitchText.tsx
Normal file
72
frontend/src/components/decorative/GlitchText.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface GlitchTextProps {
|
||||||
|
children: string;
|
||||||
|
as?: "h1" | "h2" | "h3" | "h4" | "span" | "p";
|
||||||
|
className?: string;
|
||||||
|
glitchClassName?: string;
|
||||||
|
intensity?: "low" | "medium" | "high";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GlitchText({
|
||||||
|
children,
|
||||||
|
as: Component = "span",
|
||||||
|
className,
|
||||||
|
glitchClassName,
|
||||||
|
intensity = "medium",
|
||||||
|
}: GlitchTextProps) {
|
||||||
|
const intensityStyles = {
|
||||||
|
low: "hover:animate-glitch-text",
|
||||||
|
medium: "animate-glitch-text",
|
||||||
|
high: "animate-glitch-text animate-glitch-skew",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
className={cn(
|
||||||
|
"relative inline-block font-display",
|
||||||
|
intensityStyles[intensity],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
data-text={children}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{/* Glitch layers */}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 text-[var(--color-neon-cyan)] opacity-70",
|
||||||
|
"clip-path-[inset(40%_0_60%_0)]",
|
||||||
|
"animate-glitch",
|
||||||
|
glitchClassName
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
left: "2px",
|
||||||
|
textShadow: "-2px 0 var(--color-neon-pink)",
|
||||||
|
animationDelay: "0.05s",
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 text-[var(--color-neon-pink)] opacity-70",
|
||||||
|
"clip-path-[inset(60%_0_20%_0)]",
|
||||||
|
"animate-glitch",
|
||||||
|
glitchClassName
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
left: "-2px",
|
||||||
|
textShadow: "2px 0 var(--color-neon-cyan)",
|
||||||
|
animationDelay: "0.1s",
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
frontend/src/components/decorative/GlowOrbs.tsx
Normal file
75
frontend/src/components/decorative/GlowOrbs.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface GlowOrbsProps {
|
||||||
|
className?: string;
|
||||||
|
count?: number;
|
||||||
|
colors?: ("green" | "cyan" | "purple" | "pink")[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorMap = {
|
||||||
|
green: "bg-[var(--color-neon-green)]",
|
||||||
|
cyan: "bg-[var(--color-neon-cyan)]",
|
||||||
|
purple: "bg-[var(--color-neon-purple)]",
|
||||||
|
pink: "bg-[var(--color-neon-pink)]",
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultPositions = [
|
||||||
|
{ top: "10%", left: "20%", size: "300px" },
|
||||||
|
{ top: "60%", right: "10%", size: "400px" },
|
||||||
|
{ bottom: "20%", left: "60%", size: "250px" },
|
||||||
|
{ top: "40%", left: "5%", size: "200px" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function GlowOrbs({
|
||||||
|
className,
|
||||||
|
count = 3,
|
||||||
|
colors = ["green", "cyan", "purple"],
|
||||||
|
}: GlowOrbsProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 overflow-hidden pointer-events-none",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Array.from({ length: Math.min(count, 4) }).map((_, i) => {
|
||||||
|
const color = colors[i % colors.length];
|
||||||
|
const pos = defaultPositions[i];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className={cn(
|
||||||
|
"absolute rounded-full blur-3xl opacity-20",
|
||||||
|
colorMap[color]
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width: pos.size,
|
||||||
|
height: pos.size,
|
||||||
|
top: pos.top,
|
||||||
|
left: pos.left,
|
||||||
|
right: pos.right,
|
||||||
|
bottom: pos.bottom,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
x: [0, i % 2 === 0 ? 30 : -30, 0],
|
||||||
|
y: [0, i % 2 === 0 ? -20 : 20, 0],
|
||||||
|
opacity: [0.15, 0.25, 0.15],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 8 + i * 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
delay: i * 0.5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
frontend/src/components/decorative/NeonBorder.tsx
Normal file
56
frontend/src/components/decorative/NeonBorder.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface NeonBorderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
color?: "green" | "cyan" | "purple" | "pink" | "multi";
|
||||||
|
animated?: boolean;
|
||||||
|
corners?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorStyles = {
|
||||||
|
green: "border-[var(--color-neon-green)] shadow-[var(--glow-green)]",
|
||||||
|
cyan: "border-[var(--color-neon-cyan)] shadow-[var(--glow-cyan)]",
|
||||||
|
purple: "border-[var(--color-neon-purple)] shadow-[var(--glow-purple)]",
|
||||||
|
pink: "border-[var(--color-neon-pink)] shadow-[var(--glow-pink)]",
|
||||||
|
multi: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NeonBorder({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
color = "green",
|
||||||
|
animated = false,
|
||||||
|
corners = false,
|
||||||
|
}: NeonBorderProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative border bg-card/50 backdrop-blur-sm",
|
||||||
|
color !== "multi" && colorStyles[color],
|
||||||
|
animated && "animate-neon-border-pulse",
|
||||||
|
corners && "corner-accent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Multi-color gradient border */}
|
||||||
|
{color === "multi" && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 -z-10 rounded-[inherit] p-[1px]"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(45deg, var(--color-neon-green), var(--color-neon-cyan), var(--color-neon-purple), var(--color-neon-pink))",
|
||||||
|
backgroundSize: animated ? "400% 400%" : "100% 100%",
|
||||||
|
animation: animated ? "gradient-rotate 4s linear infinite" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="h-full w-full rounded-[inherit] bg-card" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
frontend/src/components/decorative/TypewriterText.tsx
Normal file
65
frontend/src/components/decorative/TypewriterText.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface TypewriterTextProps {
|
||||||
|
text: string;
|
||||||
|
className?: string;
|
||||||
|
speed?: number;
|
||||||
|
delay?: number;
|
||||||
|
cursor?: boolean;
|
||||||
|
onComplete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TypewriterText({
|
||||||
|
text,
|
||||||
|
className,
|
||||||
|
speed = 50,
|
||||||
|
delay = 0,
|
||||||
|
cursor = true,
|
||||||
|
onComplete,
|
||||||
|
}: TypewriterTextProps) {
|
||||||
|
const [displayedText, setDisplayedText] = useState("");
|
||||||
|
const [isTyping, setIsTyping] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: NodeJS.Timeout;
|
||||||
|
|
||||||
|
const startTyping = () => {
|
||||||
|
setIsTyping(true);
|
||||||
|
let currentIndex = 0;
|
||||||
|
|
||||||
|
const typeNextChar = () => {
|
||||||
|
if (currentIndex < text.length) {
|
||||||
|
setDisplayedText(text.slice(0, currentIndex + 1));
|
||||||
|
currentIndex++;
|
||||||
|
timeout = setTimeout(typeNextChar, speed);
|
||||||
|
} else {
|
||||||
|
setIsTyping(false);
|
||||||
|
onComplete?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
typeNextChar();
|
||||||
|
};
|
||||||
|
|
||||||
|
timeout = setTimeout(startTyping, delay);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [text, speed, delay, onComplete]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cn("font-mono", className)}>
|
||||||
|
{displayedText}
|
||||||
|
{cursor && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-block w-[2px] h-[1em] ml-1 bg-[var(--color-neon-green)] align-middle",
|
||||||
|
isTyping ? "animate-blink-cursor" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
frontend/src/components/decorative/index.ts
Normal file
5
frontend/src/components/decorative/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { GlitchText } from "./GlitchText";
|
||||||
|
export { GlowOrbs } from "./GlowOrbs";
|
||||||
|
export { NeonBorder } from "./NeonBorder";
|
||||||
|
export { CornerBrackets } from "./CornerBrackets";
|
||||||
|
export { TypewriterText } from "./TypewriterText";
|
||||||
@ -6,7 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { ContestTimer } from "./contest-timer";
|
import { ContestTimer } from "./contest-timer";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Calendar, Users, FileCode2 } from "lucide-react";
|
import { Calendar, Users, FileCode2, Zap, Clock, Trophy } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface Contest {
|
interface Contest {
|
||||||
@ -45,73 +45,138 @@ function formatDate(date: string): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const statusConfig = {
|
||||||
|
running: {
|
||||||
|
color: "var(--color-neon-green)",
|
||||||
|
borderColor: "border-[var(--color-neon-green)]/50",
|
||||||
|
glowColor: "shadow-[0_0_20px_rgba(0,255,136,0.15)]",
|
||||||
|
badge: { variant: "success" as const, text: "LIVE", pulse: true },
|
||||||
|
icon: Zap,
|
||||||
|
},
|
||||||
|
upcoming: {
|
||||||
|
color: "var(--color-neon-cyan)",
|
||||||
|
borderColor: "border-[var(--color-neon-cyan)]/30",
|
||||||
|
glowColor: "",
|
||||||
|
badge: { variant: "cyan" as const, text: "СКОРО", pulse: false },
|
||||||
|
icon: Clock,
|
||||||
|
},
|
||||||
|
ended: {
|
||||||
|
color: "var(--color-neon-purple)",
|
||||||
|
borderColor: "border-[var(--color-border)]",
|
||||||
|
glowColor: "",
|
||||||
|
badge: { variant: "purple" as const, text: "ЗАВЕРШЁН", pulse: false },
|
||||||
|
icon: Trophy,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function ContestCard({ contest, className }: ContestCardProps) {
|
export function ContestCard({ contest, className }: ContestCardProps) {
|
||||||
const status = getContestStatus(contest);
|
const status = getContestStatus(contest);
|
||||||
|
const config = statusConfig[status];
|
||||||
const progress =
|
const progress =
|
||||||
contest.problems_count && contest.user_solved !== undefined
|
contest.problems_count && contest.user_solved !== undefined
|
||||||
? (contest.user_solved / contest.problems_count) * 100
|
? (contest.user_solved / contest.problems_count) * 100
|
||||||
: 0;
|
: 0;
|
||||||
|
const StatusIcon = config.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ y: -4 }}
|
whileHover={{ y: -4, scale: 1.01 }}
|
||||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||||
>
|
>
|
||||||
<Link href={`/contests/${contest.id}`}>
|
<Link href={`/contests/${contest.id}`}>
|
||||||
<Card
|
<Card
|
||||||
|
variant="cyber"
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer transition-shadow hover:shadow-lg",
|
"cursor-pointer transition-all duration-300 relative overflow-hidden group",
|
||||||
status === "running" && "border-success/50",
|
config.borderColor,
|
||||||
|
config.glowColor,
|
||||||
|
"hover:border-[var(--color-neon-green)]/50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{/* Top accent line */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 right-0 h-[2px] opacity-60"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(90deg, transparent, ${config.color}, transparent)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Corner decorations */}
|
||||||
|
<div className="absolute top-0 left-0 w-4 h-4">
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 w-full h-[1px]"
|
||||||
|
style={{ background: config.color }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 h-full w-[1px]"
|
||||||
|
style={{ background: config.color }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-0 right-0 w-4 h-4">
|
||||||
|
<div
|
||||||
|
className="absolute top-0 right-0 w-full h-[1px]"
|
||||||
|
style={{ background: config.color }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute top-0 right-0 h-full w-[1px]"
|
||||||
|
style={{ background: config.color }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<CardTitle className="line-clamp-1 text-lg">
|
<div className="flex items-center gap-3">
|
||||||
{contest.title}
|
<div
|
||||||
</CardTitle>
|
className="w-8 h-8 rounded-lg flex items-center justify-center transition-colors"
|
||||||
{status === "running" && (
|
style={{
|
||||||
<Badge variant="success" pulse className="shrink-0">
|
backgroundColor: `color-mix(in srgb, ${config.color} 15%, transparent)`,
|
||||||
LIVE
|
border: `1px solid ${config.color}40`,
|
||||||
</Badge>
|
}}
|
||||||
)}
|
>
|
||||||
{status === "upcoming" && (
|
<StatusIcon className="h-4 w-4" style={{ color: config.color }} />
|
||||||
<Badge variant="info" className="shrink-0">
|
</div>
|
||||||
Скоро
|
<CardTitle className="line-clamp-1 text-lg font-display group-hover:text-[var(--color-neon-green)] transition-colors">
|
||||||
</Badge>
|
{contest.title}
|
||||||
)}
|
</CardTitle>
|
||||||
{status === "ended" && (
|
</div>
|
||||||
<Badge variant="secondary" className="shrink-0">
|
<Badge
|
||||||
Завершён
|
variant={config.badge.variant}
|
||||||
</Badge>
|
pulse={config.badge.pulse}
|
||||||
)}
|
className="shrink-0 font-mono text-xs"
|
||||||
|
>
|
||||||
|
{config.badge.text}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* Timer */}
|
{/* Timer */}
|
||||||
{status !== "ended" && (
|
{status !== "ended" && (
|
||||||
<ContestTimer
|
<div className="p-3 rounded-lg bg-[var(--color-secondary)]/50 border border-[var(--color-border)]">
|
||||||
startTime={new Date(contest.start_time)}
|
<ContestTimer
|
||||||
endTime={new Date(contest.end_time)}
|
startTime={new Date(contest.start_time)}
|
||||||
size="sm"
|
endTime={new Date(contest.end_time)}
|
||||||
/>
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
<div className="flex items-center gap-4 text-sm font-mono">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
<Calendar className="h-4 w-4" />
|
<Calendar className="h-4 w-4 text-[var(--color-neon-cyan)]" />
|
||||||
<span>{formatDate(contest.start_time)}</span>
|
<span>{formatDate(contest.start_time)}</span>
|
||||||
</div>
|
</div>
|
||||||
{contest.problems_count !== undefined && (
|
{contest.problems_count !== undefined && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
<FileCode2 className="h-4 w-4" />
|
<FileCode2 className="h-4 w-4 text-[var(--color-neon-purple)]" />
|
||||||
<span>{contest.problems_count} задач</span>
|
<span>{contest.problems_count}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{contest.participants_count !== undefined && (
|
{contest.participants_count !== undefined && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
<Users className="h-4 w-4" />
|
<Users className="h-4 w-4 text-[var(--color-neon-pink)]" />
|
||||||
<span>{contest.participants_count}</span>
|
<span>{contest.participants_count}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -119,16 +184,35 @@ export function ContestCard({ contest, className }: ContestCardProps) {
|
|||||||
|
|
||||||
{/* Progress (if user has participated) */}
|
{/* Progress (if user has participated) */}
|
||||||
{contest.user_solved !== undefined && contest.problems_count && (
|
{contest.user_solved !== undefined && contest.problems_count && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between text-xs">
|
<div className="flex justify-between text-xs font-mono">
|
||||||
<span className="text-muted-foreground">Прогресс</span>
|
<span className="text-muted-foreground">PROGRESS</span>
|
||||||
<span>
|
<span className="text-[var(--color-neon-green)]">
|
||||||
{contest.user_solved}/{contest.problems_count}
|
{contest.user_solved}/{contest.problems_count}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={progress} className="h-1" />
|
<div className="relative h-2 bg-[var(--color-secondary)] rounded-full overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-y-0 left-0 bg-gradient-to-r from-[var(--color-neon-green)] to-[var(--color-neon-cyan)] rounded-full"
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${progress}%` }}
|
||||||
|
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||||
|
style={{
|
||||||
|
boxShadow: "0 0 10px var(--color-neon-green)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Hover effect line */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-0 left-0 right-0 h-[2px] opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(90deg, transparent, var(--color-neon-green), transparent)`,
|
||||||
|
boxShadow: "0 0 10px var(--color-neon-green)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Clock } from "lucide-react";
|
import { Clock, Hourglass, CheckCircle } from "lucide-react";
|
||||||
|
|
||||||
interface ContestTimerProps {
|
interface ContestTimerProps {
|
||||||
endTime: Date;
|
endTime: Date;
|
||||||
@ -12,28 +13,41 @@ interface ContestTimerProps {
|
|||||||
size?: "sm" | "default" | "lg";
|
size?: "sm" | "default" | "lg";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimeLeft(ms: number): string {
|
function formatTimeLeft(ms: number): { value: string; parts: { days?: number; hours: string; minutes: string; seconds: string } } {
|
||||||
if (ms <= 0) return "00:00:00";
|
if (ms <= 0) return { value: "00:00:00", parts: { hours: "00", minutes: "00", seconds: "00" } };
|
||||||
|
|
||||||
const seconds = Math.floor((ms / 1000) % 60);
|
const seconds = Math.floor((ms / 1000) % 60);
|
||||||
const minutes = Math.floor((ms / (1000 * 60)) % 60);
|
const minutes = Math.floor((ms / (1000 * 60)) % 60);
|
||||||
const hours = Math.floor((ms / (1000 * 60 * 60)) % 24);
|
const hours = Math.floor((ms / (1000 * 60 * 60)) % 24);
|
||||||
const days = Math.floor(ms / (1000 * 60 * 60 * 24));
|
const days = Math.floor(ms / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
const parts = {
|
||||||
|
days: days > 0 ? days : undefined,
|
||||||
|
hours: hours.toString().padStart(2, "0"),
|
||||||
|
minutes: minutes.toString().padStart(2, "0"),
|
||||||
|
seconds: seconds.toString().padStart(2, "0"),
|
||||||
|
};
|
||||||
|
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
return `${days}д ${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
return {
|
||||||
|
value: `${days}д ${parts.hours}:${parts.minutes}:${parts.seconds}`,
|
||||||
|
parts,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
return {
|
||||||
|
value: `${parts.hours}:${parts.minutes}:${parts.seconds}`,
|
||||||
|
parts,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTimerColor(ms: number): string {
|
function getTimerColor(ms: number): string {
|
||||||
const minutes = ms / (1000 * 60);
|
const minutes = ms / (1000 * 60);
|
||||||
|
|
||||||
if (minutes > 60) return "text-success";
|
if (minutes > 60) return "var(--color-neon-green)";
|
||||||
if (minutes > 30) return "text-warning";
|
if (minutes > 30) return "var(--color-neon-yellow)";
|
||||||
if (minutes > 5) return "text-orange-500";
|
if (minutes > 5) return "var(--color-neon-orange)";
|
||||||
return "text-destructive";
|
return "var(--color-destructive)";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTimerPulse(ms: number): boolean {
|
function getTimerPulse(ms: number): boolean {
|
||||||
@ -85,30 +99,110 @@ export function ContestTimer({
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [endTime, startTime]);
|
}, [endTime, startTime]);
|
||||||
|
|
||||||
const colorClass = status === "ended"
|
const color = status === "ended"
|
||||||
? "text-muted-foreground"
|
? "var(--color-muted-foreground)"
|
||||||
: status === "upcoming"
|
: status === "upcoming"
|
||||||
? "text-info"
|
? "var(--color-neon-cyan)"
|
||||||
: getTimerColor(timeLeft);
|
: getTimerColor(timeLeft);
|
||||||
|
|
||||||
const shouldPulse = status === "running" && getTimerPulse(timeLeft);
|
const shouldPulse = status === "running" && getTimerPulse(timeLeft);
|
||||||
|
const { parts } = formatTimeLeft(timeLeft);
|
||||||
|
|
||||||
|
const TimerIcon = status === "ended" ? CheckCircle : status === "upcoming" ? Hourglass : Clock;
|
||||||
|
|
||||||
|
if (size === "lg") {
|
||||||
|
return (
|
||||||
|
<div className={cn("font-mono", className)}>
|
||||||
|
{/* Large timer display with separate segments */}
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
{showIcon && (
|
||||||
|
<TimerIcon
|
||||||
|
className={cn("h-6 w-6 mr-2", shouldPulse && "animate-pulse")}
|
||||||
|
style={{ color }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{parts.days !== undefined && (
|
||||||
|
<>
|
||||||
|
<TimerSegment value={parts.days.toString()} label="DAYS" color={color} />
|
||||||
|
<span className="text-2xl" style={{ color }}>:</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TimerSegment value={parts.hours} label="HRS" color={color} />
|
||||||
|
<span className="text-2xl animate-pulse" style={{ color }}>:</span>
|
||||||
|
<TimerSegment value={parts.minutes} label="MIN" color={color} />
|
||||||
|
<span className="text-2xl animate-pulse" style={{ color }}>:</span>
|
||||||
|
<TimerSegment value={parts.seconds} label="SEC" color={color} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status === "upcoming" && (
|
||||||
|
<p className="text-center text-xs text-muted-foreground mt-2 font-mono">
|
||||||
|
До начала контеста
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-2 font-mono",
|
"inline-flex items-center gap-2 font-mono",
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
colorClass,
|
|
||||||
shouldPulse && "animate-pulse",
|
shouldPulse && "animate-pulse",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
style={{ color }}
|
||||||
>
|
>
|
||||||
{showIcon && <Clock className="h-4 w-4" />}
|
{showIcon && <TimerIcon className="h-4 w-4" />}
|
||||||
<span>
|
<span>
|
||||||
{status === "ended" && "Завершён"}
|
{status === "ended" && (
|
||||||
{status === "upcoming" && `До начала: ${formatTimeLeft(timeLeft)}`}
|
<span className="text-muted-foreground">ЗАВЕРШЁН</span>
|
||||||
{status === "running" && formatTimeLeft(timeLeft)}
|
)}
|
||||||
|
{status === "upcoming" && (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground mr-1">До начала:</span>
|
||||||
|
<span style={{ color, textShadow: `0 0 10px ${color}` }}>
|
||||||
|
{formatTimeLeft(timeLeft).value}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{status === "running" && (
|
||||||
|
<span style={{ textShadow: `0 0 10px ${color}` }}>
|
||||||
|
{formatTimeLeft(timeLeft).value}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TimerSegment({ value, label, color }: { value: string; label: string; color: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<motion.div
|
||||||
|
key={value}
|
||||||
|
initial={{ y: -10, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="text-3xl font-display font-bold px-3 py-2 rounded-lg bg-[var(--color-secondary)]/50 border border-[var(--color-border)]"
|
||||||
|
style={{
|
||||||
|
color,
|
||||||
|
textShadow: `0 0 20px ${color}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
{/* Glow effect */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-lg opacity-20 blur-md"
|
||||||
|
style={{ background: color }}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
<span className="text-[10px] text-muted-foreground mt-1 tracking-wider">{label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
151
frontend/src/components/effects/MatrixRain.tsx
Normal file
151
frontend/src/components/effects/MatrixRain.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useCallback } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface MatrixRainProps {
|
||||||
|
className?: string;
|
||||||
|
density?: "light" | "medium" | "dense";
|
||||||
|
color?: "green" | "cyan" | "multi";
|
||||||
|
speed?: "slow" | "normal" | "fast";
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHARS =
|
||||||
|
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ{}[]<>/*-+";
|
||||||
|
|
||||||
|
const colorSchemes = {
|
||||||
|
green: ["#00ff88", "#00cc66", "#009944"],
|
||||||
|
cyan: ["#00f5ff", "#00c4cc", "#009999"],
|
||||||
|
multi: ["#00ff88", "#00f5ff", "#bf00ff"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const speedConfig = {
|
||||||
|
slow: { interval: 80, dropSpeed: 0.3 },
|
||||||
|
normal: { interval: 50, dropSpeed: 0.5 },
|
||||||
|
fast: { interval: 30, dropSpeed: 0.8 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const densityConfig = {
|
||||||
|
light: 30,
|
||||||
|
medium: 20,
|
||||||
|
dense: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MatrixRain({
|
||||||
|
className,
|
||||||
|
density = "medium",
|
||||||
|
color = "green",
|
||||||
|
speed = "normal",
|
||||||
|
}: MatrixRainProps) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const animationRef = useRef<number>();
|
||||||
|
const dropsRef = useRef<number[]>([]);
|
||||||
|
|
||||||
|
const draw = useCallback(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const { dropSpeed } = speedConfig[speed];
|
||||||
|
const colors = colorSchemes[color];
|
||||||
|
const fontSize = 14;
|
||||||
|
const columns = Math.floor(canvas.width / fontSize);
|
||||||
|
|
||||||
|
// Initialize drops if needed
|
||||||
|
if (dropsRef.current.length !== columns) {
|
||||||
|
dropsRef.current = Array(columns)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => Math.random() * -100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semi-transparent black to create fade effect
|
||||||
|
ctx.fillStyle = "rgba(10, 10, 15, 0.05)";
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
ctx.font = `${fontSize}px monospace`;
|
||||||
|
|
||||||
|
for (let i = 0; i < dropsRef.current.length; i++) {
|
||||||
|
const char = CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||||
|
const x = i * fontSize;
|
||||||
|
const y = dropsRef.current[i] * fontSize;
|
||||||
|
|
||||||
|
// Gradient effect - brighter at the head
|
||||||
|
const headColor = colors[0];
|
||||||
|
const tailColor = colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
|
||||||
|
// Draw the head character (brightest)
|
||||||
|
ctx.fillStyle = headColor;
|
||||||
|
ctx.shadowColor = headColor;
|
||||||
|
ctx.shadowBlur = 10;
|
||||||
|
ctx.fillText(char, x, y);
|
||||||
|
|
||||||
|
// Draw a few trailing characters with decreasing opacity
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
for (let j = 1; j < 5; j++) {
|
||||||
|
const trailY = y - j * fontSize;
|
||||||
|
if (trailY > 0) {
|
||||||
|
ctx.fillStyle = `${tailColor}${Math.floor((1 - j * 0.2) * 255)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
const trailChar = CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||||
|
ctx.fillText(trailChar, x, trailY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the drop
|
||||||
|
dropsRef.current[i] += dropSpeed;
|
||||||
|
|
||||||
|
// Reset drop when it goes off screen (with some randomness)
|
||||||
|
if (
|
||||||
|
dropsRef.current[i] * fontSize > canvas.height &&
|
||||||
|
Math.random() > 0.975
|
||||||
|
) {
|
||||||
|
dropsRef.current[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [color, speed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const resizeCanvas = () => {
|
||||||
|
canvas.width = canvas.offsetWidth;
|
||||||
|
canvas.height = canvas.offsetHeight;
|
||||||
|
dropsRef.current = []; // Reset drops on resize
|
||||||
|
};
|
||||||
|
|
||||||
|
resizeCanvas();
|
||||||
|
window.addEventListener("resize", resizeCanvas);
|
||||||
|
|
||||||
|
const { interval } = speedConfig[speed];
|
||||||
|
let lastTime = 0;
|
||||||
|
|
||||||
|
const animate = (currentTime: number) => {
|
||||||
|
if (currentTime - lastTime >= interval) {
|
||||||
|
draw();
|
||||||
|
lastTime = currentTime;
|
||||||
|
}
|
||||||
|
animationRef.current = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
animationRef.current = requestAnimationFrame(animate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", resizeCanvas);
|
||||||
|
if (animationRef.current) {
|
||||||
|
cancelAnimationFrame(animationRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [draw, speed, density]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className={cn("absolute inset-0 w-full h-full opacity-30", className)}
|
||||||
|
style={{ background: "transparent" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
frontend/src/components/effects/ScanlineOverlay.tsx
Normal file
48
frontend/src/components/effects/ScanlineOverlay.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface ScanlineOverlayProps {
|
||||||
|
className?: string;
|
||||||
|
intensity?: "light" | "medium" | "heavy";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScanlineOverlay({
|
||||||
|
className,
|
||||||
|
intensity = "light",
|
||||||
|
}: ScanlineOverlayProps) {
|
||||||
|
const opacityMap = {
|
||||||
|
light: "0.03",
|
||||||
|
medium: "0.06",
|
||||||
|
heavy: "0.1",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 pointer-events-none z-50 overflow-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Scanlines */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
background: `repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
transparent,
|
||||||
|
transparent 2px,
|
||||||
|
rgba(0, 0, 0, ${opacityMap[intensity]}) 2px,
|
||||||
|
rgba(0, 0, 0, ${opacityMap[intensity]}) 4px
|
||||||
|
)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Moving scanline */}
|
||||||
|
<div
|
||||||
|
className="absolute left-0 right-0 h-[2px] bg-[var(--color-neon-green)]/10 animate-scanline"
|
||||||
|
style={{ boxShadow: "0 0 10px var(--color-neon-green)" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
frontend/src/components/effects/index.ts
Normal file
2
frontend/src/components/effects/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { MatrixRain } from "./MatrixRain";
|
||||||
|
export { ScanlineOverlay } from "./ScanlineOverlay";
|
||||||
204
frontend/src/components/illustrations/AuthIllustration.tsx
Normal file
204
frontend/src/components/illustrations/AuthIllustration.tsx
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface AuthIllustrationProps {
|
||||||
|
className?: string;
|
||||||
|
type?: "login" | "register";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthIllustration({ className, type = "login" }: AuthIllustrationProps) {
|
||||||
|
const isRegister = type === "register";
|
||||||
|
const primaryColor = isRegister ? "var(--color-neon-cyan)" : "var(--color-neon-green)";
|
||||||
|
const secondaryColor = isRegister ? "var(--color-neon-purple)" : "var(--color-neon-cyan)";
|
||||||
|
return (
|
||||||
|
<div className={cn("relative w-full max-w-sm mx-auto", className)}>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 300 250"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-full h-auto"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id={`lockGradient-${type}`} x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor={primaryColor} />
|
||||||
|
<stop offset="100%" stopColor={secondaryColor} />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id={`authGlow-${type}`}>
|
||||||
|
<feGaussianBlur stdDeviation="4" result="coloredBlur" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<radialGradient id={`centerGlow-${type}`} cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stopColor={primaryColor} stopOpacity="0.2" />
|
||||||
|
<stop offset="100%" stopColor={primaryColor} stopOpacity="0" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* Center glow */}
|
||||||
|
<circle cx="150" cy="125" r="80" fill={`url(#centerGlow-${type})`} />
|
||||||
|
|
||||||
|
{/* Outer rotating ring */}
|
||||||
|
<motion.circle
|
||||||
|
cx="150"
|
||||||
|
cy="125"
|
||||||
|
r="90"
|
||||||
|
stroke={primaryColor}
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeDasharray="20 10"
|
||||||
|
fill="none"
|
||||||
|
opacity="0.3"
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
||||||
|
style={{ transformOrigin: "150px 125px" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Middle ring */}
|
||||||
|
<motion.circle
|
||||||
|
cx="150"
|
||||||
|
cy="125"
|
||||||
|
r="70"
|
||||||
|
stroke={secondaryColor}
|
||||||
|
strokeWidth="1"
|
||||||
|
strokeDasharray="15 8"
|
||||||
|
fill="none"
|
||||||
|
opacity="0.4"
|
||||||
|
animate={{ rotate: -360 }}
|
||||||
|
transition={{ duration: 15, repeat: Infinity, ease: "linear" }}
|
||||||
|
style={{ transformOrigin: "150px 125px" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Inner hexagon */}
|
||||||
|
<motion.path
|
||||||
|
d="M150 65 L195 90 L195 160 L150 185 L105 160 L105 90 Z"
|
||||||
|
stroke={primaryColor}
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="none"
|
||||||
|
filter={`url(#authGlow-${type})`}
|
||||||
|
animate={{ scale: [1, 1.02, 1] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
style={{ transformOrigin: "150px 125px" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Lock body */}
|
||||||
|
<rect
|
||||||
|
x="125"
|
||||||
|
y="110"
|
||||||
|
width="50"
|
||||||
|
height="45"
|
||||||
|
rx="4"
|
||||||
|
fill="var(--color-card)"
|
||||||
|
stroke={`url(#lockGradient-${type})`}
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Lock shackle */}
|
||||||
|
<motion.path
|
||||||
|
d="M135 110 L135 95 C135 82 165 82 165 95 L165 110"
|
||||||
|
stroke={`url(#lockGradient-${type})`}
|
||||||
|
strokeWidth="4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
initial={{ y: 0 }}
|
||||||
|
animate={{ y: isRegister ? [0, -8, 0] : [0, -3, 0] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Lock keyhole */}
|
||||||
|
<circle cx="150" cy="128" r="6" fill={primaryColor} opacity="0.8" />
|
||||||
|
<rect x="148" y="130" width="4" height="12" rx="1" fill={primaryColor} opacity="0.8" />
|
||||||
|
|
||||||
|
{/* Scanning line */}
|
||||||
|
<motion.line
|
||||||
|
x1="105"
|
||||||
|
y1="125"
|
||||||
|
x2="195"
|
||||||
|
y2="125"
|
||||||
|
stroke={primaryColor}
|
||||||
|
strokeWidth="2"
|
||||||
|
opacity="0.5"
|
||||||
|
filter={`url(#authGlow-${type})`}
|
||||||
|
animate={{ y: [-40, 40, -40] }}
|
||||||
|
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Binary data floating */}
|
||||||
|
<motion.text
|
||||||
|
x="50"
|
||||||
|
y="80"
|
||||||
|
fill={primaryColor}
|
||||||
|
fontSize="10"
|
||||||
|
fontFamily="monospace"
|
||||||
|
opacity="0.4"
|
||||||
|
animate={{ y: [0, -10, 0], opacity: [0.4, 0.2, 0.4] }}
|
||||||
|
transition={{ duration: 3, repeat: Infinity }}
|
||||||
|
>
|
||||||
|
{isRegister ? "NEW_USER" : "10110"}
|
||||||
|
</motion.text>
|
||||||
|
|
||||||
|
<motion.text
|
||||||
|
x="230"
|
||||||
|
y="100"
|
||||||
|
fill={secondaryColor}
|
||||||
|
fontSize="10"
|
||||||
|
fontFamily="monospace"
|
||||||
|
opacity="0.4"
|
||||||
|
animate={{ y: [0, 10, 0], opacity: [0.3, 0.5, 0.3] }}
|
||||||
|
transition={{ duration: 4, repeat: Infinity, delay: 0.5 }}
|
||||||
|
>
|
||||||
|
{isRegister ? "INIT" : "01001"}
|
||||||
|
</motion.text>
|
||||||
|
|
||||||
|
<motion.text
|
||||||
|
x="40"
|
||||||
|
y="180"
|
||||||
|
fill="var(--color-neon-purple)"
|
||||||
|
fontSize="10"
|
||||||
|
fontFamily="monospace"
|
||||||
|
opacity="0.3"
|
||||||
|
animate={{ y: [0, -5, 0] }}
|
||||||
|
transition={{ duration: 2.5, repeat: Infinity, delay: 1 }}
|
||||||
|
>
|
||||||
|
{isRegister ? "CREATE" : "11010"}
|
||||||
|
</motion.text>
|
||||||
|
|
||||||
|
<motion.text
|
||||||
|
x="240"
|
||||||
|
y="170"
|
||||||
|
fill="var(--color-neon-pink)"
|
||||||
|
fontSize="10"
|
||||||
|
fontFamily="monospace"
|
||||||
|
opacity="0.3"
|
||||||
|
animate={{ y: [0, 5, 0] }}
|
||||||
|
transition={{ duration: 3.5, repeat: Infinity, delay: 0.8 }}
|
||||||
|
>
|
||||||
|
{isRegister ? "+++" : "00101"}
|
||||||
|
</motion.text>
|
||||||
|
|
||||||
|
{/* Corner decorations */}
|
||||||
|
<path d="M30 30 L50 30 L50 35 L35 35 L35 50 L30 50 Z" fill={primaryColor} opacity="0.5" />
|
||||||
|
<path d="M270 30 L250 30 L250 35 L265 35 L265 50 L270 50 Z" fill={primaryColor} opacity="0.5" />
|
||||||
|
<path d="M30 220 L50 220 L50 215 L35 215 L35 200 L30 200 Z" fill={secondaryColor} opacity="0.5" />
|
||||||
|
<path d="M270 220 L250 220 L250 215 L265 215 L265 200 L270 200 Z" fill={secondaryColor} opacity="0.5" />
|
||||||
|
|
||||||
|
{/* Status text */}
|
||||||
|
<motion.text
|
||||||
|
x="150"
|
||||||
|
y="220"
|
||||||
|
fill={primaryColor}
|
||||||
|
fontSize="10"
|
||||||
|
fontFamily="monospace"
|
||||||
|
textAnchor="middle"
|
||||||
|
animate={{ opacity: [0.5, 1, 0.5] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
|
>
|
||||||
|
{isRegister ? "CREATE_NEW_ACCOUNT" : "SECURE_AUTH_PROTOCOL"}
|
||||||
|
</motion.text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
147
frontend/src/components/illustrations/EmptyStateIllustration.tsx
Normal file
147
frontend/src/components/illustrations/EmptyStateIllustration.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface EmptyStateIllustrationProps {
|
||||||
|
className?: string;
|
||||||
|
type?: "no-data" | "no-contests" | "no-submissions" | "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyStateIllustration({
|
||||||
|
className,
|
||||||
|
type = "no-data",
|
||||||
|
}: EmptyStateIllustrationProps) {
|
||||||
|
const getContent = () => {
|
||||||
|
switch (type) {
|
||||||
|
case "no-contests":
|
||||||
|
return {
|
||||||
|
icon: "{ }",
|
||||||
|
text: "NO_CONTESTS_FOUND",
|
||||||
|
color: "var(--color-neon-cyan)",
|
||||||
|
};
|
||||||
|
case "no-submissions":
|
||||||
|
return {
|
||||||
|
icon: "[ ]",
|
||||||
|
text: "NO_SUBMISSIONS_YET",
|
||||||
|
color: "var(--color-neon-purple)",
|
||||||
|
};
|
||||||
|
case "error":
|
||||||
|
return {
|
||||||
|
icon: "!",
|
||||||
|
text: "ERROR_OCCURRED",
|
||||||
|
color: "var(--color-destructive)",
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
icon: "?",
|
||||||
|
text: "NO_DATA_AVAILABLE",
|
||||||
|
color: "var(--color-neon-green)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { icon, text, color } = getContent();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative w-full max-w-[200px] mx-auto py-8", className)}>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 200 150"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-full h-auto"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<filter id="emptyGlow">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* Outer circle */}
|
||||||
|
<motion.circle
|
||||||
|
cx="100"
|
||||||
|
cy="70"
|
||||||
|
r="50"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeDasharray="10 5"
|
||||||
|
fill="none"
|
||||||
|
opacity="0.3"
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
||||||
|
style={{ transformOrigin: "100px 70px" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Inner circle */}
|
||||||
|
<motion.circle
|
||||||
|
cx="100"
|
||||||
|
cy="70"
|
||||||
|
r="35"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="1"
|
||||||
|
fill="var(--color-card)"
|
||||||
|
opacity="0.8"
|
||||||
|
animate={{ scale: [1, 1.02, 1] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Icon */}
|
||||||
|
<motion.text
|
||||||
|
x="100"
|
||||||
|
y="80"
|
||||||
|
fill={color}
|
||||||
|
fontSize="28"
|
||||||
|
fontFamily="monospace"
|
||||||
|
textAnchor="middle"
|
||||||
|
filter="url(#emptyGlow)"
|
||||||
|
animate={{ opacity: [0.7, 1, 0.7] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</motion.text>
|
||||||
|
|
||||||
|
{/* Corner brackets */}
|
||||||
|
<path
|
||||||
|
d="M30 30 L45 30 L45 35 L35 35 L35 45 L30 45 Z"
|
||||||
|
fill={color}
|
||||||
|
opacity="0.3"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M170 30 L155 30 L155 35 L165 35 L165 45 L170 45 Z"
|
||||||
|
fill={color}
|
||||||
|
opacity="0.3"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M30 110 L45 110 L45 105 L35 105 L35 95 L30 95 Z"
|
||||||
|
fill={color}
|
||||||
|
opacity="0.3"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M170 110 L155 110 L155 105 L165 105 L165 95 L170 95 Z"
|
||||||
|
fill={color}
|
||||||
|
opacity="0.3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Status text */}
|
||||||
|
<motion.text
|
||||||
|
x="100"
|
||||||
|
y="135"
|
||||||
|
fill={color}
|
||||||
|
fontSize="10"
|
||||||
|
fontFamily="monospace"
|
||||||
|
textAnchor="middle"
|
||||||
|
opacity="0.7"
|
||||||
|
animate={{ opacity: [0.5, 0.8, 0.5] }}
|
||||||
|
transition={{ duration: 3, repeat: Infinity }}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</motion.text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
278
frontend/src/components/illustrations/HeroIllustration.tsx
Normal file
278
frontend/src/components/illustrations/HeroIllustration.tsx
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface HeroIllustrationProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeroIllustration({ className }: HeroIllustrationProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn("relative w-full max-w-lg mx-auto", className)}>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 400 300"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-full h-auto"
|
||||||
|
>
|
||||||
|
{/* Background glow */}
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="screenGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stopColor="var(--color-neon-green)" stopOpacity="0.3" />
|
||||||
|
<stop offset="100%" stopColor="var(--color-neon-green)" stopOpacity="0" />
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="codeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor="var(--color-neon-green)" />
|
||||||
|
<stop offset="100%" stopColor="var(--color-neon-cyan)" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* Screen glow effect */}
|
||||||
|
<ellipse cx="200" cy="150" rx="150" ry="100" fill="url(#screenGlow)" />
|
||||||
|
|
||||||
|
{/* Desk */}
|
||||||
|
<path
|
||||||
|
d="M50 250 L350 250 L380 280 L20 280 Z"
|
||||||
|
fill="var(--color-muted)"
|
||||||
|
stroke="var(--color-border)"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Monitor stand */}
|
||||||
|
<rect x="180" y="220" width="40" height="30" fill="var(--color-secondary)" />
|
||||||
|
<rect x="160" y="245" width="80" height="8" rx="2" fill="var(--color-border)" />
|
||||||
|
|
||||||
|
{/* Monitor frame */}
|
||||||
|
<rect
|
||||||
|
x="80"
|
||||||
|
y="60"
|
||||||
|
width="240"
|
||||||
|
height="160"
|
||||||
|
rx="8"
|
||||||
|
fill="var(--color-background)"
|
||||||
|
stroke="var(--color-border)"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Screen */}
|
||||||
|
<rect
|
||||||
|
x="90"
|
||||||
|
y="70"
|
||||||
|
width="220"
|
||||||
|
height="140"
|
||||||
|
rx="4"
|
||||||
|
fill="var(--color-card)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Code lines on screen */}
|
||||||
|
<motion.g
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
{/* Line numbers */}
|
||||||
|
<text x="100" y="95" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
01
|
||||||
|
</text>
|
||||||
|
<text x="100" y="110" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
02
|
||||||
|
</text>
|
||||||
|
<text x="100" y="125" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
03
|
||||||
|
</text>
|
||||||
|
<text x="100" y="140" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
04
|
||||||
|
</text>
|
||||||
|
<text x="100" y="155" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
05
|
||||||
|
</text>
|
||||||
|
<text x="100" y="170" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
06
|
||||||
|
</text>
|
||||||
|
<text x="100" y="185" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
07
|
||||||
|
</text>
|
||||||
|
<text x="100" y="200" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
08
|
||||||
|
</text>
|
||||||
|
</motion.g>
|
||||||
|
|
||||||
|
{/* Code content */}
|
||||||
|
<motion.g
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.4 }}
|
||||||
|
>
|
||||||
|
<text x="120" y="95" fill="var(--color-neon-purple)" fontSize="8" fontFamily="monospace">
|
||||||
|
def
|
||||||
|
</text>
|
||||||
|
<text x="138" y="95" fill="var(--color-neon-cyan)" fontSize="8" fontFamily="monospace">
|
||||||
|
solve
|
||||||
|
</text>
|
||||||
|
<text x="163" y="95" fill="var(--color-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
(n, arr):
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<text x="130" y="110" fill="var(--color-neon-purple)" fontSize="8" fontFamily="monospace">
|
||||||
|
for
|
||||||
|
</text>
|
||||||
|
<text x="148" y="110" fill="var(--color-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
i
|
||||||
|
</text>
|
||||||
|
<text x="155" y="110" fill="var(--color-neon-purple)" fontSize="8" fontFamily="monospace">
|
||||||
|
in
|
||||||
|
</text>
|
||||||
|
<text x="168" y="110" fill="var(--color-neon-cyan)" fontSize="8" fontFamily="monospace">
|
||||||
|
range
|
||||||
|
</text>
|
||||||
|
<text x="195" y="110" fill="var(--color-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
(n):
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<text x="140" y="125" fill="var(--color-neon-purple)" fontSize="8" fontFamily="monospace">
|
||||||
|
if
|
||||||
|
</text>
|
||||||
|
<text x="152" y="125" fill="var(--color-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
arr[i] {">"} max_val:
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<text x="150" y="140" fill="var(--color-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
max_val = arr[i]
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<text x="130" y="155" fill="var(--color-neon-purple)" fontSize="8" fontFamily="monospace">
|
||||||
|
return
|
||||||
|
</text>
|
||||||
|
<text x="162" y="155" fill="var(--color-foreground)" fontSize="8" fontFamily="monospace">
|
||||||
|
max_val
|
||||||
|
</text>
|
||||||
|
</motion.g>
|
||||||
|
|
||||||
|
{/* Cursor blinking */}
|
||||||
|
<motion.rect
|
||||||
|
x="195"
|
||||||
|
y="148"
|
||||||
|
width="2"
|
||||||
|
height="10"
|
||||||
|
fill="var(--color-neon-green)"
|
||||||
|
animate={{ opacity: [1, 0, 1] }}
|
||||||
|
transition={{ duration: 1, repeat: Infinity }}
|
||||||
|
filter="url(#glow)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Keyboard */}
|
||||||
|
<rect
|
||||||
|
x="120"
|
||||||
|
y="260"
|
||||||
|
width="160"
|
||||||
|
height="15"
|
||||||
|
rx="3"
|
||||||
|
fill="var(--color-secondary)"
|
||||||
|
stroke="var(--color-border)"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Floating code symbols */}
|
||||||
|
<motion.g
|
||||||
|
animate={{ y: [-5, 5, -5] }}
|
||||||
|
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
x="50"
|
||||||
|
y="100"
|
||||||
|
fill="var(--color-neon-green)"
|
||||||
|
fontSize="16"
|
||||||
|
fontFamily="monospace"
|
||||||
|
opacity="0.5"
|
||||||
|
filter="url(#glow)"
|
||||||
|
>
|
||||||
|
{"</>"}
|
||||||
|
</text>
|
||||||
|
</motion.g>
|
||||||
|
|
||||||
|
<motion.g
|
||||||
|
animate={{ y: [5, -5, 5] }}
|
||||||
|
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut", delay: 0.5 }}
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
x="340"
|
||||||
|
y="120"
|
||||||
|
fill="var(--color-neon-cyan)"
|
||||||
|
fontSize="14"
|
||||||
|
fontFamily="monospace"
|
||||||
|
opacity="0.5"
|
||||||
|
filter="url(#glow)"
|
||||||
|
>
|
||||||
|
{"{ }"}
|
||||||
|
</text>
|
||||||
|
</motion.g>
|
||||||
|
|
||||||
|
<motion.g
|
||||||
|
animate={{ y: [-3, 3, -3] }}
|
||||||
|
transition={{ duration: 3.5, repeat: Infinity, ease: "easeInOut", delay: 1 }}
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
x="30"
|
||||||
|
y="180"
|
||||||
|
fill="var(--color-neon-purple)"
|
||||||
|
fontSize="12"
|
||||||
|
fontFamily="monospace"
|
||||||
|
opacity="0.4"
|
||||||
|
filter="url(#glow)"
|
||||||
|
>
|
||||||
|
01
|
||||||
|
</text>
|
||||||
|
</motion.g>
|
||||||
|
|
||||||
|
<motion.g
|
||||||
|
animate={{ y: [3, -3, 3] }}
|
||||||
|
transition={{ duration: 4.5, repeat: Infinity, ease: "easeInOut", delay: 1.5 }}
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
x="355"
|
||||||
|
y="200"
|
||||||
|
fill="var(--color-neon-pink)"
|
||||||
|
fontSize="12"
|
||||||
|
fontFamily="monospace"
|
||||||
|
opacity="0.4"
|
||||||
|
filter="url(#glow)"
|
||||||
|
>
|
||||||
|
[]
|
||||||
|
</text>
|
||||||
|
</motion.g>
|
||||||
|
|
||||||
|
{/* Circuit lines */}
|
||||||
|
<motion.path
|
||||||
|
d="M30 230 L60 230 L60 200 L80 200"
|
||||||
|
stroke="var(--color-neon-green)"
|
||||||
|
strokeWidth="1"
|
||||||
|
fill="none"
|
||||||
|
opacity="0.3"
|
||||||
|
initial={{ pathLength: 0 }}
|
||||||
|
animate={{ pathLength: 1 }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, repeatType: "reverse" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<motion.path
|
||||||
|
d="M370 230 L340 230 L340 200 L320 200"
|
||||||
|
stroke="var(--color-neon-cyan)"
|
||||||
|
strokeWidth="1"
|
||||||
|
fill="none"
|
||||||
|
opacity="0.3"
|
||||||
|
initial={{ pathLength: 0 }}
|
||||||
|
animate={{ pathLength: 1 }}
|
||||||
|
transition={{ duration: 2, delay: 0.5, repeat: Infinity, repeatType: "reverse" }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
158
frontend/src/components/illustrations/TrophyIllustration.tsx
Normal file
158
frontend/src/components/illustrations/TrophyIllustration.tsx
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface TrophyIllustrationProps {
|
||||||
|
className?: string;
|
||||||
|
rank?: 1 | 2 | 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rankColors = {
|
||||||
|
1: { primary: "var(--color-neon-green)", secondary: "var(--color-neon-cyan)" },
|
||||||
|
2: "var(--color-neon-cyan)",
|
||||||
|
3: "var(--color-neon-purple)",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TrophyIllustration({ className, rank = 1 }: TrophyIllustrationProps) {
|
||||||
|
const color = typeof rankColors[rank] === "object" ? rankColors[rank].primary : rankColors[rank];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative w-full max-w-[120px] mx-auto", className)}>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 100 120"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-full h-auto"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id={`trophyGradient${rank}`} x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor={color} />
|
||||||
|
<stop offset="100%" stopColor={color} stopOpacity="0.5" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id={`trophyGlow${rank}`}>
|
||||||
|
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* Glow effect behind trophy */}
|
||||||
|
<motion.ellipse
|
||||||
|
cx="50"
|
||||||
|
cy="60"
|
||||||
|
rx="35"
|
||||||
|
ry="40"
|
||||||
|
fill={color}
|
||||||
|
opacity="0.1"
|
||||||
|
animate={{ scale: [1, 1.1, 1], opacity: [0.1, 0.15, 0.1] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Trophy cup */}
|
||||||
|
<motion.path
|
||||||
|
d="M25 20 L25 50 C25 65 35 75 50 75 C65 75 75 65 75 50 L75 20 Z"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="var(--color-card)"
|
||||||
|
filter={`url(#trophyGlow${rank})`}
|
||||||
|
animate={{ y: [0, -2, 0] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Trophy handles */}
|
||||||
|
<motion.path
|
||||||
|
d="M25 25 C10 25 10 45 25 45"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="none"
|
||||||
|
animate={{ y: [0, -2, 0] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
<motion.path
|
||||||
|
d="M75 25 C90 25 90 45 75 45"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="none"
|
||||||
|
animate={{ y: [0, -2, 0] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Trophy stem */}
|
||||||
|
<motion.rect
|
||||||
|
x="42"
|
||||||
|
y="75"
|
||||||
|
width="16"
|
||||||
|
height="15"
|
||||||
|
fill="var(--color-secondary)"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="1"
|
||||||
|
animate={{ y: [0, -2, 0] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Trophy base */}
|
||||||
|
<motion.path
|
||||||
|
d="M30 90 L70 90 L75 105 L25 105 Z"
|
||||||
|
fill="var(--color-secondary)"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="2"
|
||||||
|
animate={{ y: [0, -2, 0] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Rank number */}
|
||||||
|
<motion.text
|
||||||
|
x="50"
|
||||||
|
y="55"
|
||||||
|
fill={color}
|
||||||
|
fontSize="24"
|
||||||
|
fontFamily="var(--font-display)"
|
||||||
|
textAnchor="middle"
|
||||||
|
filter={`url(#trophyGlow${rank})`}
|
||||||
|
animate={{ y: [0, -2, 0] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
{rank}
|
||||||
|
</motion.text>
|
||||||
|
|
||||||
|
{/* Sparkles */}
|
||||||
|
<motion.circle
|
||||||
|
cx="20"
|
||||||
|
cy="15"
|
||||||
|
r="2"
|
||||||
|
fill={color}
|
||||||
|
animate={{ opacity: [0, 1, 0], scale: [0.5, 1, 0.5] }}
|
||||||
|
transition={{ duration: 1.5, repeat: Infinity, delay: 0 }}
|
||||||
|
/>
|
||||||
|
<motion.circle
|
||||||
|
cx="80"
|
||||||
|
cy="20"
|
||||||
|
r="1.5"
|
||||||
|
fill={color}
|
||||||
|
animate={{ opacity: [0, 1, 0], scale: [0.5, 1, 0.5] }}
|
||||||
|
transition={{ duration: 1.5, repeat: Infinity, delay: 0.5 }}
|
||||||
|
/>
|
||||||
|
<motion.circle
|
||||||
|
cx="15"
|
||||||
|
cy="60"
|
||||||
|
r="1"
|
||||||
|
fill={color}
|
||||||
|
animate={{ opacity: [0, 1, 0], scale: [0.5, 1, 0.5] }}
|
||||||
|
transition={{ duration: 1.5, repeat: Infinity, delay: 1 }}
|
||||||
|
/>
|
||||||
|
<motion.circle
|
||||||
|
cx="85"
|
||||||
|
cy="55"
|
||||||
|
r="1.5"
|
||||||
|
fill={color}
|
||||||
|
animate={{ opacity: [0, 1, 0], scale: [0.5, 1, 0.5] }}
|
||||||
|
transition={{ duration: 1.5, repeat: Infinity, delay: 0.3 }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
frontend/src/components/illustrations/index.ts
Normal file
4
frontend/src/components/illustrations/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { HeroIllustration } from "./HeroIllustration";
|
||||||
|
export { AuthIllustration } from "./AuthIllustration";
|
||||||
|
export { TrophyIllustration } from "./TrophyIllustration";
|
||||||
|
export { EmptyStateIllustration } from "./EmptyStateIllustration";
|
||||||
@ -3,23 +3,57 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
"inline-flex items-center border px-2.5 py-0.5 text-xs font-mono uppercase tracking-wider transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
// Default - neon green
|
||||||
default:
|
default:
|
||||||
"border-transparent bg-primary text-primary-foreground shadow",
|
"border-[var(--color-neon-green)]/50 bg-[var(--color-neon-green)]/10 text-[var(--color-neon-green)]",
|
||||||
|
|
||||||
|
// Secondary - muted
|
||||||
secondary:
|
secondary:
|
||||||
"border-transparent bg-secondary text-secondary-foreground",
|
"border-[var(--color-border)] bg-[var(--color-secondary)] text-[var(--color-secondary-foreground)]",
|
||||||
|
|
||||||
|
// Destructive - red
|
||||||
destructive:
|
destructive:
|
||||||
"border-transparent bg-destructive text-destructive-foreground shadow",
|
"border-[var(--color-destructive)]/50 bg-[var(--color-destructive)]/10 text-[var(--color-destructive)]",
|
||||||
outline: "text-foreground",
|
|
||||||
|
// Outline - just border
|
||||||
|
outline:
|
||||||
|
"border-[var(--color-border)] bg-transparent text-[var(--color-foreground)]",
|
||||||
|
|
||||||
|
// Success - green with glow
|
||||||
success:
|
success:
|
||||||
"border-transparent bg-success text-success-foreground shadow",
|
"border-[var(--color-success)]/50 bg-[var(--color-success)]/10 text-[var(--color-success)] shadow-[0_0_10px_rgba(0,255,136,0.2)]",
|
||||||
|
|
||||||
|
// Warning - yellow
|
||||||
warning:
|
warning:
|
||||||
"border-transparent bg-warning text-warning-foreground shadow",
|
"border-[var(--color-warning)]/50 bg-[var(--color-warning)]/10 text-[var(--color-warning)]",
|
||||||
|
|
||||||
|
// Info - cyan
|
||||||
info:
|
info:
|
||||||
"border-transparent bg-info text-info-foreground shadow",
|
"border-[var(--color-info)]/50 bg-[var(--color-info)]/10 text-[var(--color-info)]",
|
||||||
|
|
||||||
|
// Live - animated pulsing green
|
||||||
|
live:
|
||||||
|
"border-[var(--color-neon-green)] bg-[var(--color-neon-green)]/20 text-[var(--color-neon-green)] shadow-[0_0_15px_rgba(0,255,136,0.4)]",
|
||||||
|
|
||||||
|
// Purple/violet
|
||||||
|
purple:
|
||||||
|
"border-[var(--color-neon-purple)]/50 bg-[var(--color-neon-purple)]/10 text-[var(--color-neon-purple)]",
|
||||||
|
|
||||||
|
// Cyan
|
||||||
|
cyan:
|
||||||
|
"border-[var(--color-neon-cyan)]/50 bg-[var(--color-neon-cyan)]/10 text-[var(--color-neon-cyan)]",
|
||||||
|
|
||||||
|
// Pink
|
||||||
|
pink:
|
||||||
|
"border-[var(--color-neon-pink)]/50 bg-[var(--color-neon-pink)]/10 text-[var(--color-neon-pink)]",
|
||||||
|
|
||||||
|
// Orange
|
||||||
|
orange:
|
||||||
|
"border-[var(--color-neon-orange)]/50 bg-[var(--color-neon-orange)]/10 text-[var(--color-neon-orange)]",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@ -32,14 +66,16 @@ export interface BadgeProps
|
|||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
VariantProps<typeof badgeVariants> {
|
VariantProps<typeof badgeVariants> {
|
||||||
pulse?: boolean;
|
pulse?: boolean;
|
||||||
|
glow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Badge({ className, variant, pulse, ...props }: BadgeProps) {
|
function Badge({ className, variant, pulse, glow, ...props }: BadgeProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
badgeVariants({ variant }),
|
badgeVariants({ variant }),
|
||||||
pulse && "animate-pulse",
|
pulse && "animate-neon-pulse",
|
||||||
|
glow && variant === "live" && "animate-neon-border-pulse",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -7,29 +7,62 @@ import { cn } from "@/lib/utils";
|
|||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98]",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-mono uppercase tracking-wider transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-neon-green)] focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98] border",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
// Primary cyber button - filled with glow
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90 hover:shadow-md",
|
"bg-[var(--color-neon-green)] text-[var(--color-background)] border-transparent hover:shadow-[var(--glow-green)] hover:bg-[var(--color-neon-green)]/90",
|
||||||
|
|
||||||
|
// Cyber outline - transparent with neon border
|
||||||
|
cyber:
|
||||||
|
"bg-transparent border-[var(--color-neon-green)] text-[var(--color-neon-green)] hover:bg-[var(--color-neon-green)]/10 hover:shadow-[var(--glow-green)]",
|
||||||
|
|
||||||
|
// Neon cyan variant
|
||||||
|
neon:
|
||||||
|
"bg-transparent border-[var(--color-neon-cyan)] text-[var(--color-neon-cyan)] hover:bg-[var(--color-neon-cyan)]/10 hover:shadow-[var(--glow-cyan)]",
|
||||||
|
|
||||||
|
// Ghost with neon hover
|
||||||
|
"ghost-neon":
|
||||||
|
"border-transparent bg-transparent text-[var(--color-neon-green)] hover:bg-[var(--color-neon-green)]/10",
|
||||||
|
|
||||||
|
// Destructive with red neon
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
"bg-[var(--color-destructive)]/20 border-[var(--color-destructive)] text-[var(--color-destructive)] hover:bg-[var(--color-destructive)] hover:text-white hover:shadow-[0_0_20px_rgba(255,51,102,0.5)]",
|
||||||
|
|
||||||
|
// Standard outline
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
"border-[var(--color-border)] bg-transparent hover:border-[var(--color-neon-green)] hover:text-[var(--color-neon-green)] hover:bg-[var(--color-neon-green)]/5",
|
||||||
|
|
||||||
|
// Secondary muted
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
"bg-[var(--color-secondary)] border-[var(--color-border)] text-[var(--color-secondary-foreground)] hover:border-[var(--color-neon-purple)] hover:text-[var(--color-neon-purple)]",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
// Ghost - minimal
|
||||||
|
ghost:
|
||||||
|
"border-transparent hover:bg-[var(--color-accent)] hover:text-[var(--color-accent-foreground)]",
|
||||||
|
|
||||||
|
// Link style
|
||||||
|
link:
|
||||||
|
"text-[var(--color-neon-cyan)] underline-offset-4 hover:underline border-transparent",
|
||||||
|
|
||||||
|
// Success - green neon filled
|
||||||
success:
|
success:
|
||||||
"bg-success text-success-foreground shadow-sm hover:bg-success/90",
|
"bg-[var(--color-success)]/20 border-[var(--color-success)] text-[var(--color-success)] hover:bg-[var(--color-success)] hover:text-[var(--color-background)] hover:shadow-[var(--glow-green)]",
|
||||||
|
|
||||||
|
// Warning - yellow neon
|
||||||
warning:
|
warning:
|
||||||
"bg-warning text-warning-foreground shadow-sm hover:bg-warning/90",
|
"bg-[var(--color-warning)]/20 border-[var(--color-warning)] text-[var(--color-warning)] hover:bg-[var(--color-warning)] hover:text-[var(--color-background)] hover:shadow-[var(--glow-yellow)]",
|
||||||
|
|
||||||
|
// Info - cyan neon
|
||||||
|
info:
|
||||||
|
"bg-[var(--color-info)]/20 border-[var(--color-info)] text-[var(--color-info)] hover:bg-[var(--color-info)] hover:text-[var(--color-background)] hover:shadow-[var(--glow-cyan)]",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
default: "h-10 px-6 py-2",
|
||||||
sm: "h-9 rounded-md px-3 text-xs",
|
sm: "h-9 px-4 text-xs",
|
||||||
lg: "h-11 rounded-lg px-8",
|
lg: "h-12 px-8 text-base",
|
||||||
icon: "h-10 w-10",
|
icon: "h-10 w-10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -48,7 +81,19 @@ export interface ButtonProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, loading = false, children, disabled, ...props }, ref) => {
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
loading = false,
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
// When using asChild with Slot, we can only pass a single child element
|
// When using asChild with Slot, we can only pass a single child element
|
||||||
if (asChild && !loading) {
|
if (asChild && !loading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,19 +1,45 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
HTMLDivElement,
|
glowing?: boolean;
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
variant?: "default" | "cyber" | "terminal";
|
||||||
>(({ className, ...props }, ref) => (
|
}
|
||||||
<div
|
|
||||||
ref={ref}
|
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||||
className={cn(
|
({ className, glowing, variant = "default", ...props }, ref) => (
|
||||||
"rounded-xl border border-border bg-card text-card-foreground shadow-sm transition-shadow hover:shadow-md",
|
<div
|
||||||
className
|
ref={ref}
|
||||||
)}
|
className={cn(
|
||||||
{...props}
|
// Base styles
|
||||||
/>
|
"relative border bg-card/80 backdrop-blur-sm text-card-foreground transition-all duration-300",
|
||||||
));
|
// Default variant
|
||||||
|
variant === "default" && [
|
||||||
|
"border-border/50",
|
||||||
|
"hover:border-[var(--color-neon-green)]/30",
|
||||||
|
"hover:shadow-[var(--shadow-card-hover)]",
|
||||||
|
],
|
||||||
|
// Cyber variant - with corner accents
|
||||||
|
variant === "cyber" && [
|
||||||
|
"border-[var(--color-neon-green)]/30",
|
||||||
|
"hover:border-[var(--color-neon-green)]/60",
|
||||||
|
"hover:shadow-[var(--glow-green)]",
|
||||||
|
"corner-accent",
|
||||||
|
],
|
||||||
|
// Terminal variant - for code/logs
|
||||||
|
variant === "terminal" && [
|
||||||
|
"border-[var(--color-border)]",
|
||||||
|
"bg-[var(--color-background)]/90",
|
||||||
|
"font-mono",
|
||||||
|
],
|
||||||
|
// Glowing state
|
||||||
|
glowing && "animate-neon-border-pulse border-[var(--color-neon-green)]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
Card.displayName = "Card";
|
Card.displayName = "Card";
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
const CardHeader = React.forwardRef<
|
||||||
@ -34,7 +60,10 @@ const CardTitle = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<h3
|
<h3
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
className={cn(
|
||||||
|
"font-semibold leading-none tracking-wide text-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
@ -72,4 +101,11 @@ const CardFooter = React.forwardRef<
|
|||||||
));
|
));
|
||||||
CardFooter.displayName = "CardFooter";
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
|
|||||||
@ -21,12 +21,23 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background transition-colors",
|
// Base styles
|
||||||
"file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
|
"flex h-10 w-full border bg-[var(--color-input)] px-3 py-2 text-sm font-mono transition-all duration-200",
|
||||||
"placeholder:text-muted-foreground",
|
"text-foreground",
|
||||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
// Placeholder
|
||||||
|
"placeholder:text-muted-foreground/50",
|
||||||
|
// File input
|
||||||
|
"file:border-0 file:bg-transparent file:text-sm file:font-mono file:text-foreground",
|
||||||
|
// Focus state - neon glow
|
||||||
|
"focus-visible:outline-none focus-visible:border-[var(--color-neon-green)] focus-visible:shadow-[var(--glow-green)]",
|
||||||
|
// Disabled state
|
||||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
error && "border-destructive focus-visible:ring-destructive",
|
// Default border
|
||||||
|
"border-[var(--color-border)]",
|
||||||
|
// Error state
|
||||||
|
error &&
|
||||||
|
"border-[var(--color-destructive)] focus-visible:border-[var(--color-destructive)] focus-visible:shadow-[0_0_20px_rgba(255,51,102,0.4)]",
|
||||||
|
// Icon padding
|
||||||
icon && "pl-10",
|
icon && "pl-10",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user