feat: Implement a cyberpunk-themed UI with new decorative components, illustrations, and visual effects.

This commit is contained in:
n.tolstov 2025-11-30 23:18:39 +03:00
parent c49e56b1e7
commit 059e6eedf9
30 changed files with 3935 additions and 1049 deletions

View File

@ -7,7 +7,10 @@ import { ContestCard } from "@/components/domain/contest-card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Skeleton } from "@/components/ui/skeleton";
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";
const containerVariants = {
@ -59,10 +62,13 @@ export default function ContestsPage() {
if (isLoading) {
return (
<div className="container mx-auto px-4 py-8">
<Skeleton className="h-10 w-48 mb-8" />
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<Skeleton key={i} className="h-32 w-full rounded-xl" />
<div className="flex items-center gap-4 mb-8">
<Skeleton className="h-10 w-10 rounded-lg" />
<Skeleton className="h-10 w-48" />
</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>
@ -78,42 +84,100 @@ export default function ContestsPage() {
}
return (
<div className="container mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold">Контесты</h1>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<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 className="min-h-[calc(100vh-4rem)] relative">
{/* Background */}
<GlowOrbs />
<div className="absolute inset-0 cyber-grid opacity-10" />
<div className="container mx-auto px-4 py-8 relative">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-8"
>
<div className="flex items-center gap-4">
<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">
<Trophy className="h-6 w-6 text-[var(--color-neon-green)]" />
</div>
<div>
<h1 className="text-3xl font-display font-bold">
<GlitchText text="КОНТЕСТЫ" intensity="low" />
</h1>
<p className="text-sm font-mono text-muted-foreground">
<span className="text-[var(--color-neon-cyan)]">$</span> active_competitions.list()
</p>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
<TabsList>
<TabsTrigger value="all" className="gap-2">
{/* 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>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-8">
<TabsList className="bg-[var(--color-card)] border border-[var(--color-border)] p-1">
<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" />
Все ({contests.length})
<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">
<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" />
Активные ({activeContests.length})
<span className="hidden sm:inline">Предстоящие</span>
<Badge variant="cyan" className="ml-1 font-mono text-xs">
{upcomingContests.length}
</Badge>
</TabsTrigger>
<TabsTrigger value="upcoming" className="gap-2">
<Calendar className="h-4 w-4" />
Предстоящие ({upcomingContests.length})
</TabsTrigger>
<TabsTrigger value="past" className="gap-2">
<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" />
Прошедшие ({pastContests.length})
<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-4 md:grid-cols-2 lg:grid-cols-3"
className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"
variants={containerVariants}
initial="hidden"
animate="visible"
@ -135,11 +199,21 @@ export default function ContestsPage() {
))}
</motion.div>
) : (
<div className="text-center text-muted-foreground py-16">
<Calendar className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="text-lg">Контестов в этой категории пока нет</p>
</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)]">&gt;</span> Контестов в этой категории пока нет
</p>
<p className="text-sm font-mono text-muted-foreground/60 mt-2">
// check back later
</p>
</motion.div>
)}
</div>
</div>
);
}

View File

@ -1,84 +1,237 @@
@import "tailwindcss";
@theme {
/* Base colors */
--color-background: #ffffff;
--color-foreground: #0a0a0a;
--color-primary: #2563eb;
--color-primary-foreground: #ffffff;
--color-secondary: #f1f5f9;
--color-secondary-foreground: #0f172a;
--color-muted: #f1f5f9;
--color-muted-foreground: #64748b;
--color-accent: #f1f5f9;
--color-accent-foreground: #0f172a;
--color-destructive: #ef4444;
/* ========================================
CYBERPUNK THEME - ВолГУ.Контесты
======================================== */
/* Base colors - Dark theme by default */
--color-background: #0a0a0f;
--color-foreground: #e4e4e7;
/* Neon accent colors */
--color-neon-green: #00ff88;
--color-neon-cyan: #00f5ff;
--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-popover: #ffffff;
--color-popover-foreground: #0a0a0a;
--color-card: #ffffff;
--color-card-foreground: #0a0a0a;
--color-border: #e2e8f0;
--color-input: #e2e8f0;
--color-ring: #2563eb;
--radius: 0.5rem;
--color-popover: #0f0f1a;
--color-popover-foreground: #e4e4e7;
--color-card: #0f0f1a;
--color-card-foreground: #e4e4e7;
--color-border: #2a2a4a;
--color-input: #1a1a2e;
--color-ring: #00ff88;
--radius: 0.25rem;
/* Additional semantic colors */
--color-success: #22c55e;
--color-success-foreground: #ffffff;
--color-warning: #f59e0b;
--color-warning-foreground: #000000;
--color-info: #3b82f6;
--color-info-foreground: #ffffff;
/* Semantic status colors */
--color-success: #00ff88;
--color-success-foreground: #0a0a0f;
--color-warning: #f0ff00;
--color-warning-foreground: #0a0a0f;
--color-info: #00f5ff;
--color-info-foreground: #0a0a0f;
/* Card shadow */
--shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-card-hover: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
/* Glow effects */
--glow-green: 0 0 20px rgba(0, 255, 136, 0.5);
--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 {
--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);
}
}
/* ========================================
BASE STYLES
======================================== */
body {
background: var(--color-background);
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 {
0% {
background-position: -200% 0;
@ -102,13 +255,13 @@ body {
@keyframes pulse-ring {
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% {
box-shadow: 0 0 0 10px rgba(34, 197, 94, 0);
box-shadow: 0 0 0 10px rgba(0, 255, 136, 0);
}
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 {
background: linear-gradient(
90deg,
@ -145,15 +345,266 @@ body {
animation: spin 1s linear infinite;
}
/* Focus visible styles for accessibility */
.focus-visible-ring:focus-visible {
outline: none;
ring: 2px;
ring-color: var(--color-ring);
ring-offset: 2px;
/* ========================================
GLOW UTILITY CLASSES
======================================== */
.glow-green {
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 {
width: 8px;
height: 8px;
@ -165,25 +616,87 @@ body {
}
::-webkit-scrollbar-thumb {
background: var(--color-muted-foreground);
background: var(--color-border);
border-radius: 4px;
border: 1px solid var(--color-muted);
}
::-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] {
transition: background-color 0.2s ease;
transition: all 0.2s ease;
}
[data-panel-resize-handle-enabled]:hover {
background-color: var(--color-primary) !important;
background-color: var(--color-neon-green) !important;
opacity: 0.5;
box-shadow: var(--glow-green);
}
[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;
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);
}

View File

@ -1,12 +1,27 @@
import type { Metadata } from "next";
import { JetBrains_Mono, Orbitron } from "next/font/google";
import "./globals.css";
import { Navbar } from "@/components/Navbar";
import { AuthProvider } from "@/lib/auth-context";
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 = {
title: "ВолГУ.Контесты — Соревнования по программированию",
description: "Платформа для проведения соревнований по олимпиадному программированию от Волгоградского государственного университета",
description:
"Платформа для проведения соревнований по олимпиадному программированию от Волгоградского государственного университета",
};
export default function RootLayout({
@ -15,11 +30,17 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="ru">
<body className="min-h-screen bg-background antialiased">
<html lang="ru" className={`${jetbrainsMono.variable} ${orbitron.variable}`}>
<body className="min-h-screen bg-background antialiased font-mono">
<AuthProvider>
{/* Cyber grid background overlay */}
<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>
<Toaster
position="bottom-right"
@ -27,8 +48,9 @@ export default function RootLayout({
closeButton
toastOptions={{
duration: 4000,
className: "font-sans",
className: "font-mono border border-border bg-card",
}}
theme="dark"
/>
</body>
</html>

View File

@ -11,7 +11,10 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
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() {
const [email, setEmail] = useState("");
@ -39,29 +42,85 @@ export default function LoginPage() {
};
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">
{/* Background effects */}
<GlowOrbs />
<div className="absolute inset-0 cyber-grid opacity-20" />
{/* Decorative elements */}
<div className="absolute top-20 left-10 hidden lg:block">
<motion.div
animate={{ y: [0, -10, 0] }}
transition={{ duration: 3, repeat: Infinity }}
className="text-[var(--color-neon-green)]/20 font-mono text-xs"
>
{"// authenticating..."}
</motion.div>
</div>
<div className="absolute bottom-20 right-10 hidden lg:block">
<motion.div
animate={{ opacity: [0.2, 0.5, 0.2] }}
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>
</div>
</motion.div>
{/* Right side - Login form */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="w-full max-w-md"
className="w-full max-w-md mx-auto"
>
<Card className="shadow-lg">
<CardHeader className="space-y-1 text-center pb-4">
<Card variant="cyber" className="relative overflow-hidden">
{/* Card glow effect */}
<div className="absolute inset-0 bg-gradient-to-br from-[var(--color-neon-green)]/5 via-transparent to-[var(--color-neon-cyan)]/5" />
<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="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center"
className="relative mx-auto mb-4"
>
<Trophy className="h-8 w-8 text-primary" />
<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>
</motion.div>
<CardTitle className="text-2xl font-bold">Вход</CardTitle>
<p className="text-sm text-muted-foreground">
Войдите в свой аккаунт для участия в контестах
<CardTitle className="text-2xl font-display font-bold">
<GlitchText text="АВТОРИЗАЦИЯ" intensity="low" />
</CardTitle>
<p className="text-sm text-muted-foreground font-mono">
<span className="text-[var(--color-neon-cyan)]">&gt;</span> Войдите для участия в контестах
</p>
</CardHeader>
<CardContent>
<CardContent className="relative">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<motion.div
@ -73,38 +132,42 @@ export default function LoginPage() {
)}
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<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"
placeholder="email@example.com"
className="pl-10 font-mono"
placeholder="user@example.com"
/>
</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" />
<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"
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-foreground transition-colors"
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 ? (
@ -116,26 +179,48 @@ export default function LoginPage() {
</div>
</div>
<Button type="submit" className="w-full" loading={isLoading}>
<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>
<div className="mt-6 text-center">
<p className="text-sm text-muted-foreground">
{/* 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-primary hover:underline font-medium"
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>
);
}

View File

@ -4,65 +4,72 @@ import Link from "next/link";
import { motion } from "framer-motion";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useAuth } from "@/lib/auth-context";
import {
Trophy,
Zap,
Code2,
Users,
CheckCircle2,
ArrowRight,
Terminal,
Timer,
BarChart3,
Cpu,
Shield,
Sparkles,
} 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 = [
{
icon: Trophy,
title: "Соревнования",
description: "Участвуйте в контестах и соревнуйтесь с другими программистами",
color: "text-yellow-500",
bgColor: "bg-yellow-500/10",
color: "var(--color-neon-green)",
},
{
icon: Zap,
title: "Автопроверка",
description: "Мгновенная проверка решений с детальными результатами по каждому тесту",
color: "text-blue-500",
bgColor: "bg-blue-500/10",
color: "var(--color-neon-cyan)",
},
{
icon: Code2,
title: "30+ языков",
description: "Python, C++, Java, JavaScript, Go, Rust и многие другие языки",
color: "text-green-500",
bgColor: "bg-green-500/10",
color: "var(--color-neon-purple)",
},
{
icon: Timer,
title: "Real-time таймеры",
description: "Следите за временем контеста и оставшимся временем в реальном времени",
color: "text-orange-500",
bgColor: "bg-orange-500/10",
color: "var(--color-neon-orange)",
},
{
icon: BarChart3,
title: "Рейтинг",
description: "Отслеживайте свой прогресс и соревнуйтесь в таблице лидеров",
color: "text-purple-500",
bgColor: "bg-purple-500/10",
color: "var(--color-neon-pink)",
},
{
icon: Terminal,
title: "Удобный редактор",
description: "Современный редактор кода с подсветкой синтаксиса и автодополнением",
color: "text-pink-500",
bgColor: "bg-pink-500/10",
color: "var(--color-neon-yellow)",
},
];
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 = {
hidden: { opacity: 0 },
visible: {
@ -82,109 +89,164 @@ export default function HomePage() {
const { user } = useAuth();
return (
<div className="min-h-[calc(100vh-4rem)]">
<div className="min-h-[calc(100vh-4rem)] relative">
{/* Hero Section */}
<section className="relative overflow-hidden">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-primary/5" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_20%,rgba(120,119,198,0.1),transparent_50%)]" />
<section className="relative overflow-hidden min-h-[90vh] flex items-center">
{/* Background Effects */}
<div className="absolute inset-0 overflow-hidden">
<MatrixRain density="light" color="green" speed="slow" />
</div>
<GlowOrbs />
<div className="container mx-auto px-4 py-24 relative">
{/* 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, y: 30 }}
animate={{ opacity: 1, y: 0 }}
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto"
>
{/* University badge */}
<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"
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-6 w-6" />
<span className="text-sm font-medium text-[#2B4B7C]">Волгоградский государственный университет</span>
<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>
<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>
{/* 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="flex flex-col sm:flex-row gap-4 justify-center"
className="mb-8"
>
<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"
<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)]">&gt;</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"
>
<Trophy className="h-5 w-5" />
<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" />
<ArrowRight className="h-5 w-5 ml-2 transition-transform group-hover:translate-x-1" />
</Link>
</Button>
{!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" />
<Button asChild variant="neon" size="lg">
<Link href="/register">
<Users className="h-5 w-5 mr-2" />
Создать аккаунт
</Link>
</Button>
)}
</motion.div>
</motion.div>
{/* Stats */}
<motion.div
initial={{ opacity: 0, y: 40 }}
initial={{ opacity: 0, y: 30 }}
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"
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}` }}
>
{[
{ 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 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>
{/* Features Section */}
<section className="py-24 bg-muted/30">
<div className="container mx-auto px-4">
<section className="py-24 relative">
<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
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
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>
<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)]">&lt;</span>
Возможности платформы
<span className="text-[var(--color-neon-green)]">/&gt;</span>
</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>
</motion.div>
@ -199,15 +261,44 @@ export default function HomePage() {
const Icon = feature.icon;
return (
<motion.div key={index} variants={itemVariants}>
<Card className="h-full hover:shadow-lg transition-shadow">
<CardContent className="pt-6">
<Card variant="cyber" className="h-full group hover:border-[var(--color-neon-green)]/50 transition-all duration-300">
<CardContent className="pt-6 relative">
{/* Icon with glow */}
<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>
<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>
</Card>
</motion.div>
@ -218,32 +309,76 @@ export default function HomePage() {
</section>
{/* CTA Section */}
<section className="py-24">
<section className="py-24 relative">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
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%)]" />
<div className="relative">
<CheckCircle2 className="h-12 w-12 mx-auto mb-6 opacity-90" />
<h2 className="text-3xl md:text-4xl font-bold mb-4">
Готовы начать?
{/* Background with cyber styling */}
<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" />
<div className="absolute inset-0 cyber-grid opacity-20" />
{/* 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>
<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)]">&gt;VOLGU.CONTESTS</span>{" "}
и участвуйте в соревнованиях
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<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"
>
{user ? "Перейти к контестам" : "Начать бесплатно"}
<ArrowRight className="h-5 w-5" />
<Button asChild variant="cyber" size="lg" className="group">
<Link href={user ? "/contests" : "/register"}>
<Shield className="h-5 w-5 mr-2" />
{user ? "Перейти к контестам" : "Начать сейчас"}
<ArrowRight className="h-5 w-5 ml-2 transition-transform group-hover:translate-x-1" />
</Link>
</Button>
</div>
</div>
</motion.div>
@ -251,9 +386,28 @@ export default function HomePage() {
</section>
{/* Footer */}
<footer className="border-t border-border py-8">
<div className="container mx-auto px-4 text-center text-sm text-muted-foreground">
<p>ВолГУ.Контесты &copy; {new Date().getFullYear()} Волгоградский государственный университет</p>
<footer className="relative border-t border-[var(--color-border)] py-8">
{/* Top border glow */}
<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)]">&copy;</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>
</footer>
</div>

View File

@ -21,7 +21,11 @@ import {
Trash2,
Save,
Loader2,
Shield,
Send,
Globe,
} from "lucide-react";
import { GlowOrbs, GlitchText } from "@/components/decorative";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
@ -126,8 +130,11 @@ export default function ProfilePage() {
if (authLoading) {
return (
<div className="container mx-auto px-4 py-8 max-w-2xl">
<Skeleton className="h-10 w-48 mb-8" />
<Skeleton className="h-96 rounded-xl" />
<div className="flex items-center gap-4 mb-8">
<Skeleton className="h-12 w-12 rounded-lg" />
<Skeleton className="h-10 w-48" />
</div>
<Skeleton className="h-[600px] rounded-xl" />
</div>
);
}
@ -141,19 +148,31 @@ export default function ProfilePage() {
: null;
return (
<div className="container mx-auto px-4 py-8 max-w-2xl">
<div className="min-h-[calc(100vh-4rem)] relative">
{/* Background */}
<GlowOrbs />
<div className="absolute inset-0 cyber-grid opacity-10" />
<div className="container mx-auto px-4 py-8 max-w-2xl relative">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<h1 className="text-3xl font-bold flex items-center gap-3">
<User className="h-8 w-8 text-primary" />
Профиль
<div className="flex items-center gap-4">
<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">
<User className="h-6 w-6 text-[var(--color-neon-cyan)]" />
</div>
<div>
<h1 className="text-3xl font-display font-bold">
<GlitchText text="ПРОФИЛЬ" intensity="low" color="var(--color-neon-cyan)" />
</h1>
<p className="text-muted-foreground mt-1">
Управляйте своими данными
<p className="text-sm font-mono text-muted-foreground">
<span className="text-[var(--color-neon-green)]">$</span> user.settings.edit()
</p>
</div>
</div>
</motion.div>
<motion.div
@ -161,21 +180,40 @@ export default function ProfilePage() {
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<Card>
<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>Личные данные</CardTitle>
<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-border">
<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="w-32 h-32 rounded-full overflow-hidden bg-muted flex items-center justify-center cursor-pointer border-4 border-background shadow-lg"
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-muted-foreground" />
<Loader2 className="h-8 w-8 animate-spin text-[var(--color-neon-cyan)]" />
) : avatarUrl ? (
<img
src={avatarUrl}
@ -183,13 +221,16 @@ export default function ProfilePage() {
className="w-full h-full object-cover"
/>
) : (
<User className="h-16 w-16 text-muted-foreground" />
<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-primary text-primary-foreground rounded-full shadow-lg hover:bg-primary/90 transition-colors"
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>
@ -204,7 +245,7 @@ export default function ProfilePage() {
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
variant="neon"
size="sm"
onClick={handleAvatarClick}
disabled={uploadingAvatar}
@ -215,115 +256,154 @@ export default function ProfilePage() {
{user.avatar_url && (
<Button
type="button"
variant="outline"
variant="destructive"
size="sm"
onClick={handleDeleteAvatar}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4 mr-2" />
Удалить
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">
JPG, PNG или GIF. Максимум 5MB.
<p className="text-xs text-muted-foreground font-mono">
// JPG, PNG или GIF. Max 5MB.
</p>
</div>
{/* Email (read-only) */}
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Label htmlFor="email" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
Email
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="email"
value={user.email}
disabled
icon={<Mail className="h-4 w-4" />}
className="pl-10 font-mono opacity-60"
/>
<p className="text-xs text-muted-foreground">
Email нельзя изменить
</div>
<p className="text-xs text-muted-foreground font-mono">
<span className="text-[var(--color-neon-orange)]">!</span> Email нельзя изменить
</p>
</div>
{/* Username */}
<div className="space-y-2">
<Label htmlFor="username">Имя пользователя</Label>
<Label htmlFor="username" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
Имя пользователя
</Label>
<div className="relative group">
<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" />
<Input
id="username"
name="username"
value={formData.username}
onChange={handleChange}
icon={<AtSign className="h-4 w-4" />}
className="pl-10 font-mono"
placeholder="username"
/>
</div>
</div>
{/* Full Name */}
<div className="space-y-2">
<Label htmlFor="full_name">ФИО</Label>
<Label htmlFor="full_name" 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-green)] transition-colors" />
<Input
id="full_name"
name="full_name"
value={formData.full_name}
onChange={handleChange}
icon={<User className="h-4 w-4" />}
className="pl-10 font-mono"
placeholder="Иванов Иван Иванович"
/>
</div>
</div>
{/* Study Group */}
<div className="space-y-2">
<Label htmlFor="study_group">Учебная группа</Label>
<Label htmlFor="study_group" className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
Учебная группа
</Label>
<div className="relative group">
<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" />
<Input
id="study_group"
name="study_group"
value={formData.study_group}
onChange={handleChange}
icon={<GraduationCap className="h-4 w-4" />}
className="pl-10 font-mono"
placeholder="ПМИб-241"
/>
</div>
</div>
{/* Social Links */}
<div className="space-y-4 pt-4 border-t border-border">
<h3 className="font-medium">Социальные сети</h3>
<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">Telegram</Label>
<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">VK</Label>
<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-4 border-t border-border">
<div className="pt-6 border-t border-[var(--color-border)]">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Роль</span>
<Badge variant={user.role === "admin" ? "default" : "secondary"}>
{user.role === "admin" ? "Администратор" : "Участник"}
<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" className="w-full" loading={saving}>
<Button type="submit" variant="cyber" className="w-full group" loading={saving}>
<Save className="h-4 w-4 mr-2" />
Сохранить изменения
</Button>
@ -332,5 +412,6 @@ export default function ProfilePage() {
</Card>
</motion.div>
</div>
</div>
);
}

View File

@ -11,7 +11,10 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
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() {
const [email, setEmail] = useState("");
@ -58,29 +61,85 @@ export default function RegisterPage() {
};
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">
{/* Background effects */}
<GlowOrbs />
<div className="absolute inset-0 cyber-grid opacity-20" />
{/* Decorative elements */}
<div className="absolute top-20 left-10 hidden lg:block">
<motion.div
animate={{ y: [0, -10, 0] }}
transition={{ duration: 3, repeat: Infinity }}
className="text-[var(--color-neon-cyan)]/20 font-mono text-xs"
>
{"// initializing new user..."}
</motion.div>
</div>
<div className="absolute bottom-20 right-10 hidden lg:block">
<motion.div
animate={{ opacity: [0.2, 0.5, 0.2] }}
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>
</div>
</motion.div>
{/* Right side - Register form */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="w-full max-w-md"
className="w-full max-w-md mx-auto"
>
<Card className="shadow-lg">
<CardHeader className="space-y-1 text-center pb-4">
<Card variant="cyber" className="relative overflow-hidden">
{/* Card glow effect */}
<div className="absolute inset-0 bg-gradient-to-br from-[var(--color-neon-cyan)]/5 via-transparent to-[var(--color-neon-purple)]/5" />
<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="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center"
className="relative mx-auto mb-4"
>
<Trophy className="h-8 w-8 text-primary" />
<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>
</motion.div>
<CardTitle className="text-2xl font-bold">Регистрация</CardTitle>
<p className="text-sm text-muted-foreground">
Создайте аккаунт для участия в соревнованиях
<CardTitle className="text-2xl font-display font-bold">
<GlitchText text="РЕГИСТРАЦИЯ" intensity="low" color="var(--color-neon-cyan)" />
</CardTitle>
<p className="text-sm text-muted-foreground font-mono">
<span className="text-[var(--color-neon-purple)]">&gt;</span> Создайте аккаунт для участия
</p>
</CardHeader>
<CardContent>
<CardContent className="relative">
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<motion.div
@ -92,54 +151,60 @@ export default function RegisterPage() {
)}
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<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"
placeholder="email@example.com"
className="pl-10 font-mono"
placeholder="user@example.com"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="username">Имя пользователя</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<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"
className="pl-10 font-mono"
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" />
<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"
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-foreground transition-colors"
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 ? (
@ -152,16 +217,18 @@ export default function RegisterPage() {
</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" />
<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"
className="pl-10 font-mono"
placeholder="••••••••"
/>
</div>
@ -172,41 +239,73 @@ export default function RegisterPage() {
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
className="space-y-1"
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 ${passwordChecks.length ? "text-success" : "text-muted-foreground"}`}>
{passwordChecks.length ? <Check className="h-3 w-3" /> : <X className="h-3 w-3" />}
<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 ${passwordChecks.match ? "text-success" : "text-destructive"}`}>
{passwordChecks.match ? <Check className="h-3 w-3" /> : <X className="h-3 w-3" />}
<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" className="w-full" loading={isLoading}>
<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>
<div className="mt-6 text-center">
<p className="text-sm text-muted-foreground">
{/* 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-primary hover:underline font-medium"
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>
);
}

View File

@ -10,7 +10,6 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { AlertError } from "@/components/ui/alert";
import { Progress } from "@/components/ui/progress";
import { SubmissionStatus } from "@/components/domain/submission-status";
import {
Table,
@ -29,6 +28,8 @@ import {
ArrowRight,
Filter,
Hash,
Activity,
Cpu,
} from "lucide-react";
import {
Select,
@ -37,6 +38,8 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { GlowOrbs, GlitchText } from "@/components/decorative";
import { EmptyStateIllustration } from "@/components/illustrations";
import type { SubmissionListItem } from "@/types";
// Stats card component
@ -45,20 +48,45 @@ function StatsCard({
label,
value,
color,
glowColor,
}: {
icon: typeof Trophy;
label: string;
value: string | number;
color?: string;
glowColor?: string;
}) {
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">
<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 className="text-xs text-muted-foreground">{label}</div>
<div className="font-semibold text-lg">{value}</div>
<div className="text-xs text-muted-foreground font-mono uppercase tracking-wider">{label}</div>
<div
className="font-display font-bold text-xl"
style={{
color: glowColor || color,
textShadow: glowColor ? `0 0 10px ${glowColor}` : undefined,
}}
>
{value}
</div>
</div>
</div>
</CardContent>
@ -116,10 +144,13 @@ export default function SubmissionsPage() {
if (isLoading) {
return (
<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">
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-20 rounded-xl" />
<Skeleton key={i} className="h-24 rounded-xl" />
))}
</div>
<Skeleton className="h-64 rounded-xl" />
@ -136,20 +167,31 @@ export default function SubmissionsPage() {
}
return (
<div className="container mx-auto px-4 py-8">
<div className="min-h-[calc(100vh-4rem)] relative">
{/* Background */}
<GlowOrbs />
<div className="absolute inset-0 cyber-grid opacity-10" />
<div className="container mx-auto px-4 py-8 relative">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
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" />
Мои решения
<div className="flex items-center gap-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">
<FileCode className="h-6 w-6 text-[var(--color-neon-purple)]" />
</div>
<div>
<h1 className="text-3xl font-display font-bold">
<GlitchText text="МОИ РЕШЕНИЯ" intensity="low" color="var(--color-neon-purple)" />
</h1>
<p className="text-muted-foreground mt-1">
История всех отправленных решений
<p className="text-sm font-mono text-muted-foreground">
<span className="text-[var(--color-neon-cyan)]">$</span> submissions.history()
</p>
</div>
</div>
</motion.div>
{/* Stats */}
@ -159,24 +201,29 @@ export default function SubmissionsPage() {
transition={{ delay: 0.1 }}
className="grid gap-4 md:grid-cols-4 mb-8"
>
<StatsCard icon={Hash} label="Всего отправок" value={stats.total} />
<StatsCard
icon={Hash}
label="Всего отправок"
value={stats.total}
glowColor="var(--color-neon-cyan)"
/>
<StatsCard
icon={CheckCircle2}
label="Принято"
value={stats.accepted}
color="text-success"
glowColor="var(--color-neon-green)"
/>
<StatsCard
icon={Trophy}
label="Частично"
value={stats.partial}
color="text-warning"
glowColor="var(--color-neon-yellow)"
/>
<StatsCard
icon={XCircle}
label="Не принято"
value={stats.failed}
color="text-destructive"
glowColor="var(--color-destructive)"
/>
</motion.div>
@ -188,15 +235,40 @@ export default function SubmissionsPage() {
transition={{ delay: 0.2 }}
className="mb-8"
>
<Card>
<CardContent className="pt-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-muted-foreground">
Процент успешных решений
<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>
<span className="font-semibold">{stats.successRate}%</span>
</div>
<Progress value={stats.successRate} className="h-2" />
<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>
@ -207,24 +279,21 @@ export default function SubmissionsPage() {
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="flex flex-col items-center justify-center py-16"
>
<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">
У вас пока нет решений
<EmptyStateIllustration type="no-submissions" />
<h2 className="text-xl font-display font-semibold mt-4 mb-2">
<span className="text-[var(--color-neon-purple)]">&gt;</span> Нет решений
</h2>
<p className="text-muted-foreground mb-6">
Отправьте своё первое решение, чтобы увидеть его здесь
<p className="text-muted-foreground font-mono text-sm mb-6">
// отправьте своё первое решение
</p>
<Button asChild>
<Button asChild variant="cyber">
<Link href="/contests">
Перейти к контестам
<ArrowRight className="h-4 w-4 ml-2" />
</Link>
</Button>
</CardContent>
</Card>
</motion.div>
) : (
<motion.div
@ -232,34 +301,43 @@ export default function SubmissionsPage() {
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<Card>
<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">
<CardTitle className="text-lg">
История отправок ({filteredSubmissions.length})
<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-40">
<Filter className="h-4 w-4 mr-2" />
<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">Все статусы</SelectItem>
<SelectItem value="accepted">Принятые</SelectItem>
<SelectItem value="partial">Частичные</SelectItem>
<SelectItem value="failed">Не принятые</SelectItem>
<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>
<TableHead className="w-20">ID</TableHead>
<TableHead>Время</TableHead>
<TableHead>Язык</TableHead>
<TableHead className="text-center">Статус</TableHead>
<TableHead className="text-right">Баллы</TableHead>
<TableHead className="text-right">Тесты</TableHead>
<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>
@ -279,21 +357,21 @@ export default function SubmissionsPage() {
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ delay: index * 0.03 }}
className="border-b border-border hover:bg-muted/50"
className="border-b border-[var(--color-border)] hover:bg-[var(--color-neon-green)]/5 transition-colors"
>
<TableCell className="font-mono text-muted-foreground">
#{submission.id}
<span className="text-[var(--color-neon-purple)]">#</span>{submission.id}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-sm">
<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">
<Badge variant="outline" className="font-mono">
{submission.language_name || "-"}
</Badge>
</TableCell>
@ -301,15 +379,16 @@ export default function SubmissionsPage() {
<SubmissionStatus status={submission.status} />
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<div className="flex items-center justify-end gap-2 font-mono">
<span
className={`font-semibold ${
scorePercent === 100
? "text-success"
className="font-semibold"
style={{
color: scorePercent === 100
? "var(--color-neon-green)"
: scorePercent > 0
? "text-warning"
: "text-muted-foreground"
}`}
? "var(--color-neon-yellow)"
: "var(--color-muted-foreground)",
}}
>
{submission.score}/{submission.total_points}
</span>
@ -329,6 +408,7 @@ export default function SubmissionsPage() {
? "warning"
: "secondary"
}
className="font-mono"
>
{submission.tests_passed}/{submission.tests_total}
</Badge>
@ -341,8 +421,10 @@ export default function SubmissionsPage() {
</Table>
{filteredSubmissions.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Нет решений с выбранным статусом
<div className="text-center py-12">
<p className="text-muted-foreground font-mono">
<span className="text-[var(--color-neon-cyan)]">&gt;</span> Нет решений с выбранным статусом
</p>
</div>
)}
</CardContent>
@ -350,5 +432,6 @@ export default function SubmissionsPage() {
</motion.div>
)}
</div>
</div>
);
}

View File

@ -21,9 +21,10 @@ import {
User,
ChevronDown,
LayoutDashboard,
Terminal,
} from "lucide-react";
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";
@ -42,22 +43,26 @@ export function Navbar() {
};
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">
{/* Logo and Nav Links */}
<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
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.2 }}
className="relative"
>
<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>
<span className="text-xl font-bold">
<span className="text-[#2B4B7C]">ВолГУ</span>
<span className="text-muted-foreground">.</span>
<span className="text-primary">Контесты</span>
</span>
<CyberBrandText />
</Link>
{user && (
@ -71,14 +76,24 @@ export function Navbar() {
key={link.href}
href={link.href}
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
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
? "text-[var(--color-neon-green)]"
: "text-muted-foreground hover:text-[var(--color-neon-green)]"
)}
>
<Icon className="h-4 w-4" />
{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>
);
})}
@ -87,14 +102,23 @@ export function Navbar() {
<Link
href="/admin"
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")
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
? "text-[var(--color-neon-purple)]"
: "text-muted-foreground hover:text-[var(--color-neon-purple)]"
)}
>
<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>
)}
</div>
@ -111,42 +135,57 @@ export function Navbar() {
) : user ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="flex items-center gap-2 px-2">
<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">
<Button
variant="ghost"
className="flex items-center gap-2 px-2 border border-transparent hover:border-[var(--color-neon-green)]/30"
>
<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">
<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">
{user.avatar_url ? (
<img
src={`${API_URL}${user.avatar_url}`}
alt={user.username}
className="w-full h-full object-cover"
className="w-full h-full object-cover rounded-full"
/>
) : (
user.username.charAt(0).toUpperCase()
)}
</div>
<span className="hidden sm:inline text-sm font-medium">
</div>
<span className="hidden sm:inline text-sm font-mono text-foreground">
{user.username}
</span>
{user.role === "admin" && (
<Badge variant="secondary" className="hidden sm:inline-flex text-xs">
Admin
<Badge variant="purple" className="hidden sm:inline-flex">
ADMIN
</Badge>
)}
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</Button>
</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">
<p className="text-sm font-medium">{user.username}</p>
<p className="text-xs text-muted-foreground">{user.email}</p>
<p className="text-sm font-mono text-[var(--color-neon-green)]">
{user.username}
</p>
<p className="text-xs text-muted-foreground font-mono">
{user.email}
</p>
</div>
<DropdownMenuSeparator />
<DropdownMenuSeparator className="bg-[var(--color-border)]" />
<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" />
Профиль
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSeparator className="bg-[var(--color-border)]" />
{/* Mobile nav links */}
<div className="md:hidden">
@ -154,7 +193,10 @@ export function Navbar() {
const Icon = link.icon;
return (
<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" />
{link.label}
</Link>
@ -163,18 +205,21 @@ export function Navbar() {
})}
{user.role === "admin" && (
<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" />
Админ
</Link>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuSeparator className="bg-[var(--color-border)]" />
</div>
<DropdownMenuItem
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" />
Выйти
@ -182,11 +227,14 @@ export function Navbar() {
</DropdownMenuContent>
</DropdownMenu>
) : (
<div className="flex items-center gap-2">
<Button variant="ghost" asChild>
<Link href="/login">Войти</Link>
<div className="flex items-center gap-3">
<Button variant="ghost" asChild className="font-mono">
<Link href="/login">
<Terminal className="h-4 w-4 mr-2" />
Войти
</Link>
</Button>
<Button asChild>
<Button asChild className="font-mono">
<Link href="/register">Регистрация</Link>
</Button>
</div>

View File

@ -1,11 +1,58 @@
export function VolguLogo({ className = "h-8 w-8" }: { className?: string }) {
"use client";
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
viewBox="0 0 198 209"
className={className}
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
-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
@ -20,4 +67,35 @@ export function VolguLogo({ className = "h-8 w-8" }: { className?: string }) {
</g>
</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">&gt;</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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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";

View File

@ -6,7 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ContestTimer } from "./contest-timer";
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";
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) {
const status = getContestStatus(contest);
const config = statusConfig[status];
const progress =
contest.problems_count && contest.user_solved !== undefined
? (contest.user_solved / contest.problems_count) * 100
: 0;
const StatusIcon = config.icon;
return (
<motion.div
whileHover={{ y: -4 }}
whileHover={{ y: -4, scale: 1.01 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Link href={`/contests/${contest.id}`}>
<Card
variant="cyber"
className={cn(
"cursor-pointer transition-shadow hover:shadow-lg",
status === "running" && "border-success/50",
"cursor-pointer transition-all duration-300 relative overflow-hidden group",
config.borderColor,
config.glowColor,
"hover:border-[var(--color-neon-green)]/50",
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">
<div className="flex items-start justify-between gap-4">
<CardTitle className="line-clamp-1 text-lg">
<div className="flex items-center gap-3">
<div
className="w-8 h-8 rounded-lg flex items-center justify-center transition-colors"
style={{
backgroundColor: `color-mix(in srgb, ${config.color} 15%, transparent)`,
border: `1px solid ${config.color}40`,
}}
>
<StatusIcon className="h-4 w-4" style={{ color: config.color }} />
</div>
<CardTitle className="line-clamp-1 text-lg font-display group-hover:text-[var(--color-neon-green)] transition-colors">
{contest.title}
</CardTitle>
{status === "running" && (
<Badge variant="success" pulse className="shrink-0">
LIVE
</div>
<Badge
variant={config.badge.variant}
pulse={config.badge.pulse}
className="shrink-0 font-mono text-xs"
>
{config.badge.text}
</Badge>
)}
{status === "upcoming" && (
<Badge variant="info" className="shrink-0">
Скоро
</Badge>
)}
{status === "ended" && (
<Badge variant="secondary" className="shrink-0">
Завершён
</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Timer */}
{status !== "ended" && (
<div className="p-3 rounded-lg bg-[var(--color-secondary)]/50 border border-[var(--color-border)]">
<ContestTimer
startTime={new Date(contest.start_time)}
endTime={new Date(contest.end_time)}
size="sm"
/>
</div>
)}
{/* Stats */}
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
<div className="flex items-center gap-4 text-sm font-mono">
<div className="flex items-center gap-1.5 text-muted-foreground">
<Calendar className="h-4 w-4 text-[var(--color-neon-cyan)]" />
<span>{formatDate(contest.start_time)}</span>
</div>
{contest.problems_count !== undefined && (
<div className="flex items-center gap-1">
<FileCode2 className="h-4 w-4" />
<span>{contest.problems_count} задач</span>
<div className="flex items-center gap-1.5 text-muted-foreground">
<FileCode2 className="h-4 w-4 text-[var(--color-neon-purple)]" />
<span>{contest.problems_count}</span>
</div>
)}
{contest.participants_count !== undefined && (
<div className="flex items-center gap-1">
<Users className="h-4 w-4" />
<div className="flex items-center gap-1.5 text-muted-foreground">
<Users className="h-4 w-4 text-[var(--color-neon-pink)]" />
<span>{contest.participants_count}</span>
</div>
)}
@ -119,16 +184,35 @@ export function ContestCard({ contest, className }: ContestCardProps) {
{/* Progress (if user has participated) */}
{contest.user_solved !== undefined && contest.problems_count && (
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-muted-foreground">Прогресс</span>
<span>
<div className="space-y-2">
<div className="flex justify-between text-xs font-mono">
<span className="text-muted-foreground">PROGRESS</span>
<span className="text-[var(--color-neon-green)]">
{contest.user_solved}/{contest.problems_count}
</span>
</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>
)}
{/* 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>
</Card>
</Link>

View File

@ -1,8 +1,9 @@
"use client";
import { useEffect, useState } from "react";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
import { Clock } from "lucide-react";
import { Clock, Hourglass, CheckCircle } from "lucide-react";
interface ContestTimerProps {
endTime: Date;
@ -12,28 +13,41 @@ interface ContestTimerProps {
size?: "sm" | "default" | "lg";
}
function formatTimeLeft(ms: number): string {
if (ms <= 0) return "00:00:00";
function formatTimeLeft(ms: number): { value: string; parts: { days?: number; hours: string; minutes: string; seconds: string } } {
if (ms <= 0) return { value: "00:00:00", parts: { hours: "00", minutes: "00", seconds: "00" } };
const seconds = Math.floor((ms / 1000) % 60);
const minutes = Math.floor((ms / (1000 * 60)) % 60);
const hours = 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) {
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 {
const minutes = ms / (1000 * 60);
if (minutes > 60) return "text-success";
if (minutes > 30) return "text-warning";
if (minutes > 5) return "text-orange-500";
return "text-destructive";
if (minutes > 60) return "var(--color-neon-green)";
if (minutes > 30) return "var(--color-neon-yellow)";
if (minutes > 5) return "var(--color-neon-orange)";
return "var(--color-destructive)";
}
function getTimerPulse(ms: number): boolean {
@ -85,30 +99,110 @@ export function ContestTimer({
return () => clearInterval(interval);
}, [endTime, startTime]);
const colorClass = status === "ended"
? "text-muted-foreground"
const color = status === "ended"
? "var(--color-muted-foreground)"
: status === "upcoming"
? "text-info"
? "var(--color-neon-cyan)"
: getTimerColor(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 (
<div
className={cn(
"inline-flex items-center gap-2 font-mono",
sizeClasses[size],
colorClass,
shouldPulse && "animate-pulse",
className
)}
style={{ color }}
>
{showIcon && <Clock className="h-4 w-4" />}
{showIcon && <TimerIcon className="h-4 w-4" />}
<span>
{status === "ended" && "Завершён"}
{status === "upcoming" && `До начала: ${formatTimeLeft(timeLeft)}`}
{status === "running" && formatTimeLeft(timeLeft)}
{status === "ended" && (
<span className="text-muted-foreground">ЗАВЕРШЁН</span>
)}
{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>
</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>
);
}

View 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" }}
/>
);
}

View 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>
);
}

View File

@ -0,0 +1,2 @@
export { MatrixRain } from "./MatrixRain";
export { ScanlineOverlay } from "./ScanlineOverlay";

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -0,0 +1,4 @@
export { HeroIllustration } from "./HeroIllustration";
export { AuthIllustration } from "./AuthIllustration";
export { TrophyIllustration } from "./TrophyIllustration";
export { EmptyStateIllustration } from "./EmptyStateIllustration";

View File

@ -3,23 +3,57 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
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: {
variant: {
// Default - neon green
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:
"border-transparent bg-secondary text-secondary-foreground",
"border-[var(--color-border)] bg-[var(--color-secondary)] text-[var(--color-secondary-foreground)]",
// Destructive - red
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow",
outline: "text-foreground",
"border-[var(--color-destructive)]/50 bg-[var(--color-destructive)]/10 text-[var(--color-destructive)]",
// Outline - just border
outline:
"border-[var(--color-border)] bg-transparent text-[var(--color-foreground)]",
// Success - green with glow
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:
"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:
"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: {
@ -32,14 +66,16 @@ export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {
pulse?: boolean;
glow?: boolean;
}
function Badge({ className, variant, pulse, ...props }: BadgeProps) {
function Badge({ className, variant, pulse, glow, ...props }: BadgeProps) {
return (
<div
className={cn(
badgeVariants({ variant }),
pulse && "animate-pulse",
pulse && "animate-neon-pulse",
glow && variant === "live" && "animate-neon-border-pulse",
className
)}
{...props}

View File

@ -7,29 +7,62 @@ import { cn } from "@/lib/utils";
import { Loader2 } from "lucide-react";
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: {
variant: {
// Primary cyber button - filled with glow
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:
"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:
"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:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
"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 - 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:
"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:
"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: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3 text-xs",
lg: "h-11 rounded-lg px-8",
default: "h-10 px-6 py-2",
sm: "h-9 px-4 text-xs",
lg: "h-12 px-8 text-base",
icon: "h-10 w-10",
},
},
@ -48,7 +81,19 @@ export interface 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
if (asChild && !loading) {
return (

View File

@ -1,19 +1,45 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
glowing?: boolean;
variant?: "default" | "cyber" | "terminal";
}
const Card = React.forwardRef<HTMLDivElement, CardProps>(
({ className, glowing, variant = "default", ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border border-border bg-card text-card-foreground shadow-sm transition-shadow hover:shadow-md",
// 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";
const CardHeader = React.forwardRef<
@ -34,7 +60,10 @@ const CardTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
className={cn(
"font-semibold leading-none tracking-wide text-foreground",
className
)}
{...props}
/>
));
@ -72,4 +101,11 @@ const CardFooter = React.forwardRef<
));
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

@ -21,12 +21,23 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
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",
"file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
"placeholder:text-muted-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
// Base styles
"flex h-10 w-full border bg-[var(--color-input)] px-3 py-2 text-sm font-mono transition-all duration-200",
"text-foreground",
// 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",
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",
className
)}