222 lines
7.9 KiB
TypeScript
222 lines
7.9 KiB
TypeScript
"use client";
|
||
|
||
import { motion } from "framer-motion";
|
||
import Link from "next/link";
|
||
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, Zap, Clock, Trophy } from "lucide-react";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
interface Contest {
|
||
id: number;
|
||
title: string;
|
||
description?: string;
|
||
start_time: string;
|
||
end_time: string;
|
||
is_active: boolean;
|
||
problems_count?: number;
|
||
participants_count?: number;
|
||
user_solved?: number;
|
||
}
|
||
|
||
interface ContestCardProps {
|
||
contest: Contest;
|
||
className?: string;
|
||
}
|
||
|
||
function getContestStatus(contest: Contest): "upcoming" | "running" | "ended" {
|
||
const now = new Date();
|
||
const start = new Date(contest.start_time);
|
||
const end = new Date(contest.end_time);
|
||
|
||
if (now < start) return "upcoming";
|
||
if (now >= end) return "ended";
|
||
return "running";
|
||
}
|
||
|
||
function formatDate(date: string): string {
|
||
return new Date(date).toLocaleString("ru-RU", {
|
||
day: "numeric",
|
||
month: "short",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
});
|
||
}
|
||
|
||
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, scale: 1.01 }}
|
||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||
>
|
||
<Link href={`/contests/${contest.id}`}>
|
||
<Card
|
||
variant="cyber"
|
||
className={cn(
|
||
"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">
|
||
<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>
|
||
</div>
|
||
<Badge
|
||
variant={config.badge.variant}
|
||
pulse={config.badge.pulse}
|
||
className="shrink-0 font-mono text-xs"
|
||
>
|
||
{config.badge.text}
|
||
</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 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.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.5 text-muted-foreground">
|
||
<Users className="h-4 w-4 text-[var(--color-neon-pink)]" />
|
||
<span>{contest.participants_count}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Progress (if user has participated) */}
|
||
{contest.user_solved !== undefined && contest.problems_count && (
|
||
<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>
|
||
<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>
|
||
</motion.div>
|
||
);
|
||
}
|