volsu-contests/frontend/src/components/domain/contest-card.tsx

222 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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