feat: Implement a cyberpunk-themed UI with new decorative components, illustrations, and visual effects.
This commit is contained in:
parent
c49e56b1e7
commit
059e6eedf9
@ -7,7 +7,10 @@ import { ContestCard } from "@/components/domain/contest-card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { 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)]">></span> Контестов в этой категории пока нет
|
||||
</p>
|
||||
<p className="text-sm font-mono text-muted-foreground/60 mt-2">
|
||||
// check back later
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)]">></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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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)]">></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)]"><</span>
|
||||
Возможности платформы
|
||||
<span className="text-[var(--color-neon-green)]">/></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)]">>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>ВолГУ.Контесты © {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)]">©</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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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)]">></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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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)]">></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)]">></span> Нет решений с выбранным статусом
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
@ -350,5 +432,6 @@ export default function SubmissionsPage() {
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
@ -16,8 +63,39 @@ export function VolguLogo({ className = "h-8 w-8" }: { className?: string }) {
|
||||
-94 -33 -129 -55 -110 -67 8 -5 22 -9 31 -9 21 0 41 -35 34 -58 -8 -24 -54
|
||||
-50 -105 -58 -38 -6 -40 -8 -26 -24 8 -9 15 -26 15 -37 0 -26 -54 -73 -82 -73
|
||||
-15 0 -19 -5 -15 -19 2 -11 65 -164 138 -340 l134 -321 30 0 c29 0 32 4 97
|
||||
158 37 86 236 551 443 1032 206 481 375 880 375 887 0 13 -10 14 -40 7z"/>
|
||||
158 37 86 236 551 443 1032 206 481 375 880 375 887 0 13 -10 14 -40 7z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
if (animated) {
|
||||
return (
|
||||
<motion.div
|
||||
animate={{
|
||||
filter: [
|
||||
"drop-shadow(0 0 5px var(--color-neon-green))",
|
||||
"drop-shadow(0 0 15px var(--color-neon-green))",
|
||||
"drop-shadow(0 0 5px var(--color-neon-green))",
|
||||
],
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
>
|
||||
{LogoSvg}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return LogoSvg;
|
||||
}
|
||||
|
||||
// Brand text component for the navbar
|
||||
export function CyberBrandText({ className }: { className?: string }) {
|
||||
return (
|
||||
<span className={cn("font-mono text-xl tracking-wider", className)}>
|
||||
<span className="text-[var(--color-neon-green)] text-glow-green">></span>
|
||||
<span className="text-[var(--color-neon-cyan)]">VOLGU</span>
|
||||
<span className="text-muted-foreground">.</span>
|
||||
<span className="text-[var(--color-neon-green)]">CONTESTS</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
78
frontend/src/components/decorative/CornerBrackets.tsx
Normal file
78
frontend/src/components/decorative/CornerBrackets.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CornerBracketsProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
color?: "green" | "cyan" | "purple";
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const colorStyles = {
|
||||
green: "border-[var(--color-neon-green)]",
|
||||
cyan: "border-[var(--color-neon-cyan)]",
|
||||
purple: "border-[var(--color-neon-purple)]",
|
||||
};
|
||||
|
||||
const sizeStyles = {
|
||||
sm: { bracket: "w-3 h-3", offset: "4px" },
|
||||
md: { bracket: "w-4 h-4", offset: "8px" },
|
||||
lg: { bracket: "w-6 h-6", offset: "12px" },
|
||||
};
|
||||
|
||||
export function CornerBrackets({
|
||||
children,
|
||||
className,
|
||||
color = "green",
|
||||
size = "md",
|
||||
}: CornerBracketsProps) {
|
||||
const { bracket, offset } = sizeStyles[size];
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)}>
|
||||
{/* Top Left */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute border-t-2 border-l-2 transition-opacity",
|
||||
bracket,
|
||||
colorStyles[color],
|
||||
"opacity-50 group-hover:opacity-100"
|
||||
)}
|
||||
style={{ top: offset, left: offset }}
|
||||
/>
|
||||
{/* Top Right */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute border-t-2 border-r-2 transition-opacity",
|
||||
bracket,
|
||||
colorStyles[color],
|
||||
"opacity-50 group-hover:opacity-100"
|
||||
)}
|
||||
style={{ top: offset, right: offset }}
|
||||
/>
|
||||
{/* Bottom Left */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute border-b-2 border-l-2 transition-opacity",
|
||||
bracket,
|
||||
colorStyles[color],
|
||||
"opacity-50 group-hover:opacity-100"
|
||||
)}
|
||||
style={{ bottom: offset, left: offset }}
|
||||
/>
|
||||
{/* Bottom Right */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute border-b-2 border-r-2 transition-opacity",
|
||||
bracket,
|
||||
colorStyles[color],
|
||||
"opacity-50 group-hover:opacity-100"
|
||||
)}
|
||||
style={{ bottom: offset, right: offset }}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
frontend/src/components/decorative/GlitchText.tsx
Normal file
72
frontend/src/components/decorative/GlitchText.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface GlitchTextProps {
|
||||
children: string;
|
||||
as?: "h1" | "h2" | "h3" | "h4" | "span" | "p";
|
||||
className?: string;
|
||||
glitchClassName?: string;
|
||||
intensity?: "low" | "medium" | "high";
|
||||
}
|
||||
|
||||
export function GlitchText({
|
||||
children,
|
||||
as: Component = "span",
|
||||
className,
|
||||
glitchClassName,
|
||||
intensity = "medium",
|
||||
}: GlitchTextProps) {
|
||||
const intensityStyles = {
|
||||
low: "hover:animate-glitch-text",
|
||||
medium: "animate-glitch-text",
|
||||
high: "animate-glitch-text animate-glitch-skew",
|
||||
};
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={cn(
|
||||
"relative inline-block font-display",
|
||||
intensityStyles[intensity],
|
||||
className
|
||||
)}
|
||||
data-text={children}
|
||||
>
|
||||
{children}
|
||||
{/* Glitch layers */}
|
||||
<span
|
||||
className={cn(
|
||||
"absolute inset-0 text-[var(--color-neon-cyan)] opacity-70",
|
||||
"clip-path-[inset(40%_0_60%_0)]",
|
||||
"animate-glitch",
|
||||
glitchClassName
|
||||
)}
|
||||
style={{
|
||||
left: "2px",
|
||||
textShadow: "-2px 0 var(--color-neon-pink)",
|
||||
animationDelay: "0.05s",
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute inset-0 text-[var(--color-neon-pink)] opacity-70",
|
||||
"clip-path-[inset(60%_0_20%_0)]",
|
||||
"animate-glitch",
|
||||
glitchClassName
|
||||
)}
|
||||
style={{
|
||||
left: "-2px",
|
||||
textShadow: "2px 0 var(--color-neon-cyan)",
|
||||
animationDelay: "0.1s",
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
75
frontend/src/components/decorative/GlowOrbs.tsx
Normal file
75
frontend/src/components/decorative/GlowOrbs.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface GlowOrbsProps {
|
||||
className?: string;
|
||||
count?: number;
|
||||
colors?: ("green" | "cyan" | "purple" | "pink")[];
|
||||
}
|
||||
|
||||
const colorMap = {
|
||||
green: "bg-[var(--color-neon-green)]",
|
||||
cyan: "bg-[var(--color-neon-cyan)]",
|
||||
purple: "bg-[var(--color-neon-purple)]",
|
||||
pink: "bg-[var(--color-neon-pink)]",
|
||||
};
|
||||
|
||||
const defaultPositions = [
|
||||
{ top: "10%", left: "20%", size: "300px" },
|
||||
{ top: "60%", right: "10%", size: "400px" },
|
||||
{ bottom: "20%", left: "60%", size: "250px" },
|
||||
{ top: "40%", left: "5%", size: "200px" },
|
||||
];
|
||||
|
||||
export function GlowOrbs({
|
||||
className,
|
||||
count = 3,
|
||||
colors = ["green", "cyan", "purple"],
|
||||
}: GlowOrbsProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 overflow-hidden pointer-events-none",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{Array.from({ length: Math.min(count, 4) }).map((_, i) => {
|
||||
const color = colors[i % colors.length];
|
||||
const pos = defaultPositions[i];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={i}
|
||||
className={cn(
|
||||
"absolute rounded-full blur-3xl opacity-20",
|
||||
colorMap[color]
|
||||
)}
|
||||
style={{
|
||||
width: pos.size,
|
||||
height: pos.size,
|
||||
top: pos.top,
|
||||
left: pos.left,
|
||||
right: pos.right,
|
||||
bottom: pos.bottom,
|
||||
}}
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
x: [0, i % 2 === 0 ? 30 : -30, 0],
|
||||
y: [0, i % 2 === 0 ? -20 : 20, 0],
|
||||
opacity: [0.15, 0.25, 0.15],
|
||||
}}
|
||||
transition={{
|
||||
duration: 8 + i * 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: i * 0.5,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
frontend/src/components/decorative/NeonBorder.tsx
Normal file
56
frontend/src/components/decorative/NeonBorder.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface NeonBorderProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
color?: "green" | "cyan" | "purple" | "pink" | "multi";
|
||||
animated?: boolean;
|
||||
corners?: boolean;
|
||||
}
|
||||
|
||||
const colorStyles = {
|
||||
green: "border-[var(--color-neon-green)] shadow-[var(--glow-green)]",
|
||||
cyan: "border-[var(--color-neon-cyan)] shadow-[var(--glow-cyan)]",
|
||||
purple: "border-[var(--color-neon-purple)] shadow-[var(--glow-purple)]",
|
||||
pink: "border-[var(--color-neon-pink)] shadow-[var(--glow-pink)]",
|
||||
multi: "",
|
||||
};
|
||||
|
||||
export function NeonBorder({
|
||||
children,
|
||||
className,
|
||||
color = "green",
|
||||
animated = false,
|
||||
corners = false,
|
||||
}: NeonBorderProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative border bg-card/50 backdrop-blur-sm",
|
||||
color !== "multi" && colorStyles[color],
|
||||
animated && "animate-neon-border-pulse",
|
||||
corners && "corner-accent",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Multi-color gradient border */}
|
||||
{color === "multi" && (
|
||||
<div
|
||||
className="absolute inset-0 -z-10 rounded-[inherit] p-[1px]"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(45deg, var(--color-neon-green), var(--color-neon-cyan), var(--color-neon-purple), var(--color-neon-pink))",
|
||||
backgroundSize: animated ? "400% 400%" : "100% 100%",
|
||||
animation: animated ? "gradient-rotate 4s linear infinite" : "none",
|
||||
}}
|
||||
>
|
||||
<div className="h-full w-full rounded-[inherit] bg-card" />
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
frontend/src/components/decorative/TypewriterText.tsx
Normal file
65
frontend/src/components/decorative/TypewriterText.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface TypewriterTextProps {
|
||||
text: string;
|
||||
className?: string;
|
||||
speed?: number;
|
||||
delay?: number;
|
||||
cursor?: boolean;
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
export function TypewriterText({
|
||||
text,
|
||||
className,
|
||||
speed = 50,
|
||||
delay = 0,
|
||||
cursor = true,
|
||||
onComplete,
|
||||
}: TypewriterTextProps) {
|
||||
const [displayedText, setDisplayedText] = useState("");
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
const startTyping = () => {
|
||||
setIsTyping(true);
|
||||
let currentIndex = 0;
|
||||
|
||||
const typeNextChar = () => {
|
||||
if (currentIndex < text.length) {
|
||||
setDisplayedText(text.slice(0, currentIndex + 1));
|
||||
currentIndex++;
|
||||
timeout = setTimeout(typeNextChar, speed);
|
||||
} else {
|
||||
setIsTyping(false);
|
||||
onComplete?.();
|
||||
}
|
||||
};
|
||||
|
||||
typeNextChar();
|
||||
};
|
||||
|
||||
timeout = setTimeout(startTyping, delay);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [text, speed, delay, onComplete]);
|
||||
|
||||
return (
|
||||
<span className={cn("font-mono", className)}>
|
||||
{displayedText}
|
||||
{cursor && (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block w-[2px] h-[1em] ml-1 bg-[var(--color-neon-green)] align-middle",
|
||||
isTyping ? "animate-blink-cursor" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
5
frontend/src/components/decorative/index.ts
Normal file
5
frontend/src/components/decorative/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { GlitchText } from "./GlitchText";
|
||||
export { GlowOrbs } from "./GlowOrbs";
|
||||
export { NeonBorder } from "./NeonBorder";
|
||||
export { CornerBrackets } from "./CornerBrackets";
|
||||
export { TypewriterText } from "./TypewriterText";
|
||||
@ -6,7 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { 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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
151
frontend/src/components/effects/MatrixRain.tsx
Normal file
151
frontend/src/components/effects/MatrixRain.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useCallback } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MatrixRainProps {
|
||||
className?: string;
|
||||
density?: "light" | "medium" | "dense";
|
||||
color?: "green" | "cyan" | "multi";
|
||||
speed?: "slow" | "normal" | "fast";
|
||||
}
|
||||
|
||||
const CHARS =
|
||||
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ{}[]<>/*-+";
|
||||
|
||||
const colorSchemes = {
|
||||
green: ["#00ff88", "#00cc66", "#009944"],
|
||||
cyan: ["#00f5ff", "#00c4cc", "#009999"],
|
||||
multi: ["#00ff88", "#00f5ff", "#bf00ff"],
|
||||
};
|
||||
|
||||
const speedConfig = {
|
||||
slow: { interval: 80, dropSpeed: 0.3 },
|
||||
normal: { interval: 50, dropSpeed: 0.5 },
|
||||
fast: { interval: 30, dropSpeed: 0.8 },
|
||||
};
|
||||
|
||||
const densityConfig = {
|
||||
light: 30,
|
||||
medium: 20,
|
||||
dense: 12,
|
||||
};
|
||||
|
||||
export function MatrixRain({
|
||||
className,
|
||||
density = "medium",
|
||||
color = "green",
|
||||
speed = "normal",
|
||||
}: MatrixRainProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const animationRef = useRef<number>();
|
||||
const dropsRef = useRef<number[]>([]);
|
||||
|
||||
const draw = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const { dropSpeed } = speedConfig[speed];
|
||||
const colors = colorSchemes[color];
|
||||
const fontSize = 14;
|
||||
const columns = Math.floor(canvas.width / fontSize);
|
||||
|
||||
// Initialize drops if needed
|
||||
if (dropsRef.current.length !== columns) {
|
||||
dropsRef.current = Array(columns)
|
||||
.fill(0)
|
||||
.map(() => Math.random() * -100);
|
||||
}
|
||||
|
||||
// Semi-transparent black to create fade effect
|
||||
ctx.fillStyle = "rgba(10, 10, 15, 0.05)";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.font = `${fontSize}px monospace`;
|
||||
|
||||
for (let i = 0; i < dropsRef.current.length; i++) {
|
||||
const char = CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||
const x = i * fontSize;
|
||||
const y = dropsRef.current[i] * fontSize;
|
||||
|
||||
// Gradient effect - brighter at the head
|
||||
const headColor = colors[0];
|
||||
const tailColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
|
||||
// Draw the head character (brightest)
|
||||
ctx.fillStyle = headColor;
|
||||
ctx.shadowColor = headColor;
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.fillText(char, x, y);
|
||||
|
||||
// Draw a few trailing characters with decreasing opacity
|
||||
ctx.shadowBlur = 0;
|
||||
for (let j = 1; j < 5; j++) {
|
||||
const trailY = y - j * fontSize;
|
||||
if (trailY > 0) {
|
||||
ctx.fillStyle = `${tailColor}${Math.floor((1 - j * 0.2) * 255)
|
||||
.toString(16)
|
||||
.padStart(2, "0")}`;
|
||||
const trailChar = CHARS[Math.floor(Math.random() * CHARS.length)];
|
||||
ctx.fillText(trailChar, x, trailY);
|
||||
}
|
||||
}
|
||||
|
||||
// Move the drop
|
||||
dropsRef.current[i] += dropSpeed;
|
||||
|
||||
// Reset drop when it goes off screen (with some randomness)
|
||||
if (
|
||||
dropsRef.current[i] * fontSize > canvas.height &&
|
||||
Math.random() > 0.975
|
||||
) {
|
||||
dropsRef.current[i] = 0;
|
||||
}
|
||||
}
|
||||
}, [color, speed]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
dropsRef.current = []; // Reset drops on resize
|
||||
};
|
||||
|
||||
resizeCanvas();
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
|
||||
const { interval } = speedConfig[speed];
|
||||
let lastTime = 0;
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
if (currentTime - lastTime >= interval) {
|
||||
draw();
|
||||
lastTime = currentTime;
|
||||
}
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", resizeCanvas);
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
};
|
||||
}, [draw, speed, density]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={cn("absolute inset-0 w-full h-full opacity-30", className)}
|
||||
style={{ background: "transparent" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
48
frontend/src/components/effects/ScanlineOverlay.tsx
Normal file
48
frontend/src/components/effects/ScanlineOverlay.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ScanlineOverlayProps {
|
||||
className?: string;
|
||||
intensity?: "light" | "medium" | "heavy";
|
||||
}
|
||||
|
||||
export function ScanlineOverlay({
|
||||
className,
|
||||
intensity = "light",
|
||||
}: ScanlineOverlayProps) {
|
||||
const opacityMap = {
|
||||
light: "0.03",
|
||||
medium: "0.06",
|
||||
heavy: "0.1",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 pointer-events-none z-50 overflow-hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Scanlines */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background: `repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(0, 0, 0, ${opacityMap[intensity]}) 2px,
|
||||
rgba(0, 0, 0, ${opacityMap[intensity]}) 4px
|
||||
)`,
|
||||
}}
|
||||
/>
|
||||
{/* Moving scanline */}
|
||||
<div
|
||||
className="absolute left-0 right-0 h-[2px] bg-[var(--color-neon-green)]/10 animate-scanline"
|
||||
style={{ boxShadow: "0 0 10px var(--color-neon-green)" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
frontend/src/components/effects/index.ts
Normal file
2
frontend/src/components/effects/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { MatrixRain } from "./MatrixRain";
|
||||
export { ScanlineOverlay } from "./ScanlineOverlay";
|
||||
204
frontend/src/components/illustrations/AuthIllustration.tsx
Normal file
204
frontend/src/components/illustrations/AuthIllustration.tsx
Normal file
@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AuthIllustrationProps {
|
||||
className?: string;
|
||||
type?: "login" | "register";
|
||||
}
|
||||
|
||||
export function AuthIllustration({ className, type = "login" }: AuthIllustrationProps) {
|
||||
const isRegister = type === "register";
|
||||
const primaryColor = isRegister ? "var(--color-neon-cyan)" : "var(--color-neon-green)";
|
||||
const secondaryColor = isRegister ? "var(--color-neon-purple)" : "var(--color-neon-cyan)";
|
||||
return (
|
||||
<div className={cn("relative w-full max-w-sm mx-auto", className)}>
|
||||
<svg
|
||||
viewBox="0 0 300 250"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-full h-auto"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={`lockGradient-${type}`} x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor={primaryColor} />
|
||||
<stop offset="100%" stopColor={secondaryColor} />
|
||||
</linearGradient>
|
||||
<filter id={`authGlow-${type}`}>
|
||||
<feGaussianBlur stdDeviation="4" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<radialGradient id={`centerGlow-${type}`} cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stopColor={primaryColor} stopOpacity="0.2" />
|
||||
<stop offset="100%" stopColor={primaryColor} stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
{/* Center glow */}
|
||||
<circle cx="150" cy="125" r="80" fill={`url(#centerGlow-${type})`} />
|
||||
|
||||
{/* Outer rotating ring */}
|
||||
<motion.circle
|
||||
cx="150"
|
||||
cy="125"
|
||||
r="90"
|
||||
stroke={primaryColor}
|
||||
strokeWidth="1"
|
||||
strokeDasharray="20 10"
|
||||
fill="none"
|
||||
opacity="0.3"
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
||||
style={{ transformOrigin: "150px 125px" }}
|
||||
/>
|
||||
|
||||
{/* Middle ring */}
|
||||
<motion.circle
|
||||
cx="150"
|
||||
cy="125"
|
||||
r="70"
|
||||
stroke={secondaryColor}
|
||||
strokeWidth="1"
|
||||
strokeDasharray="15 8"
|
||||
fill="none"
|
||||
opacity="0.4"
|
||||
animate={{ rotate: -360 }}
|
||||
transition={{ duration: 15, repeat: Infinity, ease: "linear" }}
|
||||
style={{ transformOrigin: "150px 125px" }}
|
||||
/>
|
||||
|
||||
{/* Inner hexagon */}
|
||||
<motion.path
|
||||
d="M150 65 L195 90 L195 160 L150 185 L105 160 L105 90 Z"
|
||||
stroke={primaryColor}
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
filter={`url(#authGlow-${type})`}
|
||||
animate={{ scale: [1, 1.02, 1] }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
style={{ transformOrigin: "150px 125px" }}
|
||||
/>
|
||||
|
||||
{/* Lock body */}
|
||||
<rect
|
||||
x="125"
|
||||
y="110"
|
||||
width="50"
|
||||
height="45"
|
||||
rx="4"
|
||||
fill="var(--color-card)"
|
||||
stroke={`url(#lockGradient-${type})`}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
|
||||
{/* Lock shackle */}
|
||||
<motion.path
|
||||
d="M135 110 L135 95 C135 82 165 82 165 95 L165 110"
|
||||
stroke={`url(#lockGradient-${type})`}
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
initial={{ y: 0 }}
|
||||
animate={{ y: isRegister ? [0, -8, 0] : [0, -3, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
{/* Lock keyhole */}
|
||||
<circle cx="150" cy="128" r="6" fill={primaryColor} opacity="0.8" />
|
||||
<rect x="148" y="130" width="4" height="12" rx="1" fill={primaryColor} opacity="0.8" />
|
||||
|
||||
{/* Scanning line */}
|
||||
<motion.line
|
||||
x1="105"
|
||||
y1="125"
|
||||
x2="195"
|
||||
y2="125"
|
||||
stroke={primaryColor}
|
||||
strokeWidth="2"
|
||||
opacity="0.5"
|
||||
filter={`url(#authGlow-${type})`}
|
||||
animate={{ y: [-40, 40, -40] }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
{/* Binary data floating */}
|
||||
<motion.text
|
||||
x="50"
|
||||
y="80"
|
||||
fill={primaryColor}
|
||||
fontSize="10"
|
||||
fontFamily="monospace"
|
||||
opacity="0.4"
|
||||
animate={{ y: [0, -10, 0], opacity: [0.4, 0.2, 0.4] }}
|
||||
transition={{ duration: 3, repeat: Infinity }}
|
||||
>
|
||||
{isRegister ? "NEW_USER" : "10110"}
|
||||
</motion.text>
|
||||
|
||||
<motion.text
|
||||
x="230"
|
||||
y="100"
|
||||
fill={secondaryColor}
|
||||
fontSize="10"
|
||||
fontFamily="monospace"
|
||||
opacity="0.4"
|
||||
animate={{ y: [0, 10, 0], opacity: [0.3, 0.5, 0.3] }}
|
||||
transition={{ duration: 4, repeat: Infinity, delay: 0.5 }}
|
||||
>
|
||||
{isRegister ? "INIT" : "01001"}
|
||||
</motion.text>
|
||||
|
||||
<motion.text
|
||||
x="40"
|
||||
y="180"
|
||||
fill="var(--color-neon-purple)"
|
||||
fontSize="10"
|
||||
fontFamily="monospace"
|
||||
opacity="0.3"
|
||||
animate={{ y: [0, -5, 0] }}
|
||||
transition={{ duration: 2.5, repeat: Infinity, delay: 1 }}
|
||||
>
|
||||
{isRegister ? "CREATE" : "11010"}
|
||||
</motion.text>
|
||||
|
||||
<motion.text
|
||||
x="240"
|
||||
y="170"
|
||||
fill="var(--color-neon-pink)"
|
||||
fontSize="10"
|
||||
fontFamily="monospace"
|
||||
opacity="0.3"
|
||||
animate={{ y: [0, 5, 0] }}
|
||||
transition={{ duration: 3.5, repeat: Infinity, delay: 0.8 }}
|
||||
>
|
||||
{isRegister ? "+++" : "00101"}
|
||||
</motion.text>
|
||||
|
||||
{/* Corner decorations */}
|
||||
<path d="M30 30 L50 30 L50 35 L35 35 L35 50 L30 50 Z" fill={primaryColor} opacity="0.5" />
|
||||
<path d="M270 30 L250 30 L250 35 L265 35 L265 50 L270 50 Z" fill={primaryColor} opacity="0.5" />
|
||||
<path d="M30 220 L50 220 L50 215 L35 215 L35 200 L30 200 Z" fill={secondaryColor} opacity="0.5" />
|
||||
<path d="M270 220 L250 220 L250 215 L265 215 L265 200 L270 200 Z" fill={secondaryColor} opacity="0.5" />
|
||||
|
||||
{/* Status text */}
|
||||
<motion.text
|
||||
x="150"
|
||||
y="220"
|
||||
fill={primaryColor}
|
||||
fontSize="10"
|
||||
fontFamily="monospace"
|
||||
textAnchor="middle"
|
||||
animate={{ opacity: [0.5, 1, 0.5] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
>
|
||||
{isRegister ? "CREATE_NEW_ACCOUNT" : "SECURE_AUTH_PROTOCOL"}
|
||||
</motion.text>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
147
frontend/src/components/illustrations/EmptyStateIllustration.tsx
Normal file
147
frontend/src/components/illustrations/EmptyStateIllustration.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface EmptyStateIllustrationProps {
|
||||
className?: string;
|
||||
type?: "no-data" | "no-contests" | "no-submissions" | "error";
|
||||
}
|
||||
|
||||
export function EmptyStateIllustration({
|
||||
className,
|
||||
type = "no-data",
|
||||
}: EmptyStateIllustrationProps) {
|
||||
const getContent = () => {
|
||||
switch (type) {
|
||||
case "no-contests":
|
||||
return {
|
||||
icon: "{ }",
|
||||
text: "NO_CONTESTS_FOUND",
|
||||
color: "var(--color-neon-cyan)",
|
||||
};
|
||||
case "no-submissions":
|
||||
return {
|
||||
icon: "[ ]",
|
||||
text: "NO_SUBMISSIONS_YET",
|
||||
color: "var(--color-neon-purple)",
|
||||
};
|
||||
case "error":
|
||||
return {
|
||||
icon: "!",
|
||||
text: "ERROR_OCCURRED",
|
||||
color: "var(--color-destructive)",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: "?",
|
||||
text: "NO_DATA_AVAILABLE",
|
||||
color: "var(--color-neon-green)",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const { icon, text, color } = getContent();
|
||||
|
||||
return (
|
||||
<div className={cn("relative w-full max-w-[200px] mx-auto py-8", className)}>
|
||||
<svg
|
||||
viewBox="0 0 200 150"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-full h-auto"
|
||||
>
|
||||
<defs>
|
||||
<filter id="emptyGlow">
|
||||
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Outer circle */}
|
||||
<motion.circle
|
||||
cx="100"
|
||||
cy="70"
|
||||
r="50"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeDasharray="10 5"
|
||||
fill="none"
|
||||
opacity="0.3"
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
||||
style={{ transformOrigin: "100px 70px" }}
|
||||
/>
|
||||
|
||||
{/* Inner circle */}
|
||||
<motion.circle
|
||||
cx="100"
|
||||
cy="70"
|
||||
r="35"
|
||||
stroke={color}
|
||||
strokeWidth="1"
|
||||
fill="var(--color-card)"
|
||||
opacity="0.8"
|
||||
animate={{ scale: [1, 1.02, 1] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
/>
|
||||
|
||||
{/* Icon */}
|
||||
<motion.text
|
||||
x="100"
|
||||
y="80"
|
||||
fill={color}
|
||||
fontSize="28"
|
||||
fontFamily="monospace"
|
||||
textAnchor="middle"
|
||||
filter="url(#emptyGlow)"
|
||||
animate={{ opacity: [0.7, 1, 0.7] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
>
|
||||
{icon}
|
||||
</motion.text>
|
||||
|
||||
{/* Corner brackets */}
|
||||
<path
|
||||
d="M30 30 L45 30 L45 35 L35 35 L35 45 L30 45 Z"
|
||||
fill={color}
|
||||
opacity="0.3"
|
||||
/>
|
||||
<path
|
||||
d="M170 30 L155 30 L155 35 L165 35 L165 45 L170 45 Z"
|
||||
fill={color}
|
||||
opacity="0.3"
|
||||
/>
|
||||
<path
|
||||
d="M30 110 L45 110 L45 105 L35 105 L35 95 L30 95 Z"
|
||||
fill={color}
|
||||
opacity="0.3"
|
||||
/>
|
||||
<path
|
||||
d="M170 110 L155 110 L155 105 L165 105 L165 95 L170 95 Z"
|
||||
fill={color}
|
||||
opacity="0.3"
|
||||
/>
|
||||
|
||||
{/* Status text */}
|
||||
<motion.text
|
||||
x="100"
|
||||
y="135"
|
||||
fill={color}
|
||||
fontSize="10"
|
||||
fontFamily="monospace"
|
||||
textAnchor="middle"
|
||||
opacity="0.7"
|
||||
animate={{ opacity: [0.5, 0.8, 0.5] }}
|
||||
transition={{ duration: 3, repeat: Infinity }}
|
||||
>
|
||||
{text}
|
||||
</motion.text>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
278
frontend/src/components/illustrations/HeroIllustration.tsx
Normal file
278
frontend/src/components/illustrations/HeroIllustration.tsx
Normal file
@ -0,0 +1,278 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface HeroIllustrationProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function HeroIllustration({ className }: HeroIllustrationProps) {
|
||||
return (
|
||||
<div className={cn("relative w-full max-w-lg mx-auto", className)}>
|
||||
<svg
|
||||
viewBox="0 0 400 300"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-full h-auto"
|
||||
>
|
||||
{/* Background glow */}
|
||||
<defs>
|
||||
<radialGradient id="screenGlow" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stopColor="var(--color-neon-green)" stopOpacity="0.3" />
|
||||
<stop offset="100%" stopColor="var(--color-neon-green)" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient id="codeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="var(--color-neon-green)" />
|
||||
<stop offset="100%" stopColor="var(--color-neon-cyan)" />
|
||||
</linearGradient>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Screen glow effect */}
|
||||
<ellipse cx="200" cy="150" rx="150" ry="100" fill="url(#screenGlow)" />
|
||||
|
||||
{/* Desk */}
|
||||
<path
|
||||
d="M50 250 L350 250 L380 280 L20 280 Z"
|
||||
fill="var(--color-muted)"
|
||||
stroke="var(--color-border)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
|
||||
{/* Monitor stand */}
|
||||
<rect x="180" y="220" width="40" height="30" fill="var(--color-secondary)" />
|
||||
<rect x="160" y="245" width="80" height="8" rx="2" fill="var(--color-border)" />
|
||||
|
||||
{/* Monitor frame */}
|
||||
<rect
|
||||
x="80"
|
||||
y="60"
|
||||
width="240"
|
||||
height="160"
|
||||
rx="8"
|
||||
fill="var(--color-background)"
|
||||
stroke="var(--color-border)"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
|
||||
{/* Screen */}
|
||||
<rect
|
||||
x="90"
|
||||
y="70"
|
||||
width="220"
|
||||
height="140"
|
||||
rx="4"
|
||||
fill="var(--color-card)"
|
||||
/>
|
||||
|
||||
{/* Code lines on screen */}
|
||||
<motion.g
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
{/* Line numbers */}
|
||||
<text x="100" y="95" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||
01
|
||||
</text>
|
||||
<text x="100" y="110" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||
02
|
||||
</text>
|
||||
<text x="100" y="125" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||
03
|
||||
</text>
|
||||
<text x="100" y="140" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||
04
|
||||
</text>
|
||||
<text x="100" y="155" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||
05
|
||||
</text>
|
||||
<text x="100" y="170" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||
06
|
||||
</text>
|
||||
<text x="100" y="185" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||
07
|
||||
</text>
|
||||
<text x="100" y="200" fill="var(--color-muted-foreground)" fontSize="8" fontFamily="monospace">
|
||||
08
|
||||
</text>
|
||||
</motion.g>
|
||||
|
||||
{/* Code content */}
|
||||
<motion.g
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.4 }}
|
||||
>
|
||||
<text x="120" y="95" fill="var(--color-neon-purple)" fontSize="8" fontFamily="monospace">
|
||||
def
|
||||
</text>
|
||||
<text x="138" y="95" fill="var(--color-neon-cyan)" fontSize="8" fontFamily="monospace">
|
||||
solve
|
||||
</text>
|
||||
<text x="163" y="95" fill="var(--color-foreground)" fontSize="8" fontFamily="monospace">
|
||||
(n, arr):
|
||||
</text>
|
||||
|
||||
<text x="130" y="110" fill="var(--color-neon-purple)" fontSize="8" fontFamily="monospace">
|
||||
for
|
||||
</text>
|
||||
<text x="148" y="110" fill="var(--color-foreground)" fontSize="8" fontFamily="monospace">
|
||||
i
|
||||
</text>
|
||||
<text x="155" y="110" fill="var(--color-neon-purple)" fontSize="8" fontFamily="monospace">
|
||||
in
|
||||
</text>
|
||||
<text x="168" y="110" fill="var(--color-neon-cyan)" fontSize="8" fontFamily="monospace">
|
||||
range
|
||||
</text>
|
||||
<text x="195" y="110" fill="var(--color-foreground)" fontSize="8" fontFamily="monospace">
|
||||
(n):
|
||||
</text>
|
||||
|
||||
<text x="140" y="125" fill="var(--color-neon-purple)" fontSize="8" fontFamily="monospace">
|
||||
if
|
||||
</text>
|
||||
<text x="152" y="125" fill="var(--color-foreground)" fontSize="8" fontFamily="monospace">
|
||||
arr[i] {">"} max_val:
|
||||
</text>
|
||||
|
||||
<text x="150" y="140" fill="var(--color-foreground)" fontSize="8" fontFamily="monospace">
|
||||
max_val = arr[i]
|
||||
</text>
|
||||
|
||||
<text x="130" y="155" fill="var(--color-neon-purple)" fontSize="8" fontFamily="monospace">
|
||||
return
|
||||
</text>
|
||||
<text x="162" y="155" fill="var(--color-foreground)" fontSize="8" fontFamily="monospace">
|
||||
max_val
|
||||
</text>
|
||||
</motion.g>
|
||||
|
||||
{/* Cursor blinking */}
|
||||
<motion.rect
|
||||
x="195"
|
||||
y="148"
|
||||
width="2"
|
||||
height="10"
|
||||
fill="var(--color-neon-green)"
|
||||
animate={{ opacity: [1, 0, 1] }}
|
||||
transition={{ duration: 1, repeat: Infinity }}
|
||||
filter="url(#glow)"
|
||||
/>
|
||||
|
||||
{/* Keyboard */}
|
||||
<rect
|
||||
x="120"
|
||||
y="260"
|
||||
width="160"
|
||||
height="15"
|
||||
rx="3"
|
||||
fill="var(--color-secondary)"
|
||||
stroke="var(--color-border)"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
|
||||
{/* Floating code symbols */}
|
||||
<motion.g
|
||||
animate={{ y: [-5, 5, -5] }}
|
||||
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
|
||||
>
|
||||
<text
|
||||
x="50"
|
||||
y="100"
|
||||
fill="var(--color-neon-green)"
|
||||
fontSize="16"
|
||||
fontFamily="monospace"
|
||||
opacity="0.5"
|
||||
filter="url(#glow)"
|
||||
>
|
||||
{"</>"}
|
||||
</text>
|
||||
</motion.g>
|
||||
|
||||
<motion.g
|
||||
animate={{ y: [5, -5, 5] }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut", delay: 0.5 }}
|
||||
>
|
||||
<text
|
||||
x="340"
|
||||
y="120"
|
||||
fill="var(--color-neon-cyan)"
|
||||
fontSize="14"
|
||||
fontFamily="monospace"
|
||||
opacity="0.5"
|
||||
filter="url(#glow)"
|
||||
>
|
||||
{"{ }"}
|
||||
</text>
|
||||
</motion.g>
|
||||
|
||||
<motion.g
|
||||
animate={{ y: [-3, 3, -3] }}
|
||||
transition={{ duration: 3.5, repeat: Infinity, ease: "easeInOut", delay: 1 }}
|
||||
>
|
||||
<text
|
||||
x="30"
|
||||
y="180"
|
||||
fill="var(--color-neon-purple)"
|
||||
fontSize="12"
|
||||
fontFamily="monospace"
|
||||
opacity="0.4"
|
||||
filter="url(#glow)"
|
||||
>
|
||||
01
|
||||
</text>
|
||||
</motion.g>
|
||||
|
||||
<motion.g
|
||||
animate={{ y: [3, -3, 3] }}
|
||||
transition={{ duration: 4.5, repeat: Infinity, ease: "easeInOut", delay: 1.5 }}
|
||||
>
|
||||
<text
|
||||
x="355"
|
||||
y="200"
|
||||
fill="var(--color-neon-pink)"
|
||||
fontSize="12"
|
||||
fontFamily="monospace"
|
||||
opacity="0.4"
|
||||
filter="url(#glow)"
|
||||
>
|
||||
[]
|
||||
</text>
|
||||
</motion.g>
|
||||
|
||||
{/* Circuit lines */}
|
||||
<motion.path
|
||||
d="M30 230 L60 230 L60 200 L80 200"
|
||||
stroke="var(--color-neon-green)"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
opacity="0.3"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 2, repeat: Infinity, repeatType: "reverse" }}
|
||||
/>
|
||||
|
||||
<motion.path
|
||||
d="M370 230 L340 230 L340 200 L320 200"
|
||||
stroke="var(--color-neon-cyan)"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
opacity="0.3"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 2, delay: 0.5, repeat: Infinity, repeatType: "reverse" }}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
158
frontend/src/components/illustrations/TrophyIllustration.tsx
Normal file
158
frontend/src/components/illustrations/TrophyIllustration.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface TrophyIllustrationProps {
|
||||
className?: string;
|
||||
rank?: 1 | 2 | 3;
|
||||
}
|
||||
|
||||
const rankColors = {
|
||||
1: { primary: "var(--color-neon-green)", secondary: "var(--color-neon-cyan)" },
|
||||
2: "var(--color-neon-cyan)",
|
||||
3: "var(--color-neon-purple)",
|
||||
};
|
||||
|
||||
export function TrophyIllustration({ className, rank = 1 }: TrophyIllustrationProps) {
|
||||
const color = typeof rankColors[rank] === "object" ? rankColors[rank].primary : rankColors[rank];
|
||||
|
||||
return (
|
||||
<div className={cn("relative w-full max-w-[120px] mx-auto", className)}>
|
||||
<svg
|
||||
viewBox="0 0 100 120"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-full h-auto"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={`trophyGradient${rank}`} x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor={color} />
|
||||
<stop offset="100%" stopColor={color} stopOpacity="0.5" />
|
||||
</linearGradient>
|
||||
<filter id={`trophyGlow${rank}`}>
|
||||
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Glow effect behind trophy */}
|
||||
<motion.ellipse
|
||||
cx="50"
|
||||
cy="60"
|
||||
rx="35"
|
||||
ry="40"
|
||||
fill={color}
|
||||
opacity="0.1"
|
||||
animate={{ scale: [1, 1.1, 1], opacity: [0.1, 0.15, 0.1] }}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
/>
|
||||
|
||||
{/* Trophy cup */}
|
||||
<motion.path
|
||||
d="M25 20 L25 50 C25 65 35 75 50 75 C65 75 75 65 75 50 L75 20 Z"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
fill="var(--color-card)"
|
||||
filter={`url(#trophyGlow${rank})`}
|
||||
animate={{ y: [0, -2, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
{/* Trophy handles */}
|
||||
<motion.path
|
||||
d="M25 25 C10 25 10 45 25 45"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
animate={{ y: [0, -2, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
<motion.path
|
||||
d="M75 25 C90 25 90 45 75 45"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
animate={{ y: [0, -2, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
{/* Trophy stem */}
|
||||
<motion.rect
|
||||
x="42"
|
||||
y="75"
|
||||
width="16"
|
||||
height="15"
|
||||
fill="var(--color-secondary)"
|
||||
stroke={color}
|
||||
strokeWidth="1"
|
||||
animate={{ y: [0, -2, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
{/* Trophy base */}
|
||||
<motion.path
|
||||
d="M30 90 L70 90 L75 105 L25 105 Z"
|
||||
fill="var(--color-secondary)"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
animate={{ y: [0, -2, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
{/* Rank number */}
|
||||
<motion.text
|
||||
x="50"
|
||||
y="55"
|
||||
fill={color}
|
||||
fontSize="24"
|
||||
fontFamily="var(--font-display)"
|
||||
textAnchor="middle"
|
||||
filter={`url(#trophyGlow${rank})`}
|
||||
animate={{ y: [0, -2, 0] }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
>
|
||||
{rank}
|
||||
</motion.text>
|
||||
|
||||
{/* Sparkles */}
|
||||
<motion.circle
|
||||
cx="20"
|
||||
cy="15"
|
||||
r="2"
|
||||
fill={color}
|
||||
animate={{ opacity: [0, 1, 0], scale: [0.5, 1, 0.5] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: 0 }}
|
||||
/>
|
||||
<motion.circle
|
||||
cx="80"
|
||||
cy="20"
|
||||
r="1.5"
|
||||
fill={color}
|
||||
animate={{ opacity: [0, 1, 0], scale: [0.5, 1, 0.5] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: 0.5 }}
|
||||
/>
|
||||
<motion.circle
|
||||
cx="15"
|
||||
cy="60"
|
||||
r="1"
|
||||
fill={color}
|
||||
animate={{ opacity: [0, 1, 0], scale: [0.5, 1, 0.5] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: 1 }}
|
||||
/>
|
||||
<motion.circle
|
||||
cx="85"
|
||||
cy="55"
|
||||
r="1.5"
|
||||
fill={color}
|
||||
animate={{ opacity: [0, 1, 0], scale: [0.5, 1, 0.5] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: 0.3 }}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
4
frontend/src/components/illustrations/index.ts
Normal file
4
frontend/src/components/illustrations/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { HeroIllustration } from "./HeroIllustration";
|
||||
export { AuthIllustration } from "./AuthIllustration";
|
||||
export { TrophyIllustration } from "./TrophyIllustration";
|
||||
export { EmptyStateIllustration } from "./EmptyStateIllustration";
|
||||
@ -3,23 +3,57 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
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}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user