diff --git a/backend/app/models/contest.py b/backend/app/models/contest.py
index 4977eb4..e7e88d6 100644
--- a/backend/app/models/contest.py
+++ b/backend/app/models/contest.py
@@ -18,6 +18,7 @@ class Contest(Base):
start_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
end_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=False)
+ show_leaderboard_during_contest: Mapped[bool] = mapped_column(Boolean, default=False)
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
diff --git a/backend/app/routers/leaderboard.py b/backend/app/routers/leaderboard.py
index f06e5a2..2986063 100644
--- a/backend/app/routers/leaderboard.py
+++ b/backend/app/routers/leaderboard.py
@@ -45,8 +45,8 @@ async def get_leaderboard(
if not contest:
raise HTTPException(status_code=404, detail="Contest not found")
- # Check if leaderboard should be hidden (during contest)
- is_hidden = contest.is_running and current_user.role != "admin"
+ # Check if leaderboard should be hidden (during contest, unless show_leaderboard_during_contest is True)
+ is_hidden = contest.is_running and not contest.show_leaderboard_during_contest and current_user.role != "admin"
if is_hidden:
return LeaderboardResponse(
diff --git a/backend/app/schemas/contest.py b/backend/app/schemas/contest.py
index d2451cd..39a8456 100644
--- a/backend/app/schemas/contest.py
+++ b/backend/app/schemas/contest.py
@@ -8,6 +8,7 @@ class ContestCreate(BaseModel):
start_time: datetime
end_time: datetime
is_active: bool = False
+ show_leaderboard_during_contest: bool = False
class ContestUpdate(BaseModel):
@@ -16,6 +17,7 @@ class ContestUpdate(BaseModel):
start_time: datetime | None = None
end_time: datetime | None = None
is_active: bool | None = None
+ show_leaderboard_during_contest: bool | None = None
class ContestResponse(BaseModel):
@@ -25,6 +27,7 @@ class ContestResponse(BaseModel):
start_time: datetime
end_time: datetime
is_active: bool
+ show_leaderboard_during_contest: bool
created_by: int | None
created_at: datetime
is_running: bool
@@ -43,6 +46,7 @@ class ContestListResponse(BaseModel):
start_time: datetime
end_time: datetime
is_active: bool
+ show_leaderboard_during_contest: bool
is_running: bool
has_ended: bool
problems_count: int = 0
diff --git a/frontend/src/app/admin/contests/[id]/edit/page.tsx b/frontend/src/app/admin/contests/[id]/edit/page.tsx
index 8ca321e..4864a38 100644
--- a/frontend/src/app/admin/contests/[id]/edit/page.tsx
+++ b/frontend/src/app/admin/contests/[id]/edit/page.tsx
@@ -22,6 +22,7 @@ export default function EditContestPage() {
start_time: "",
end_time: "",
is_active: false,
+ show_leaderboard_during_contest: false,
});
useEffect(() => {
@@ -39,6 +40,7 @@ export default function EditContestPage() {
start_time: formatDateTimeLocal(contest.start_time),
end_time: formatDateTimeLocal(contest.end_time),
is_active: contest.is_active,
+ show_leaderboard_during_contest: contest.show_leaderboard_during_contest,
});
})
.catch((err) => setError(err.message))
@@ -62,6 +64,7 @@ export default function EditContestPage() {
start_time: new Date(formData.start_time).toISOString(),
end_time: new Date(formData.end_time).toISOString(),
is_active: formData.is_active,
+ show_leaderboard_during_contest: formData.show_leaderboard_during_contest,
});
router.push("/admin/contests");
} catch (err) {
@@ -157,19 +160,36 @@ export default function EditContestPage() {
-
-
- setFormData({ ...formData, is_active: e.target.checked })
- }
- className="w-4 h-4"
- />
-
+
diff --git a/frontend/src/app/admin/contests/new/page.tsx b/frontend/src/app/admin/contests/new/page.tsx
index 147aa71..328ac06 100644
--- a/frontend/src/app/admin/contests/new/page.tsx
+++ b/frontend/src/app/admin/contests/new/page.tsx
@@ -17,6 +17,7 @@ export default function NewContestPage() {
start_time: "",
end_time: "",
is_active: false,
+ show_leaderboard_during_contest: false,
});
if (!user || user.role !== "admin") {
@@ -36,6 +37,7 @@ export default function NewContestPage() {
start_time: new Date(formData.start_time).toISOString(),
end_time: new Date(formData.end_time).toISOString(),
is_active: formData.is_active,
+ show_leaderboard_during_contest: formData.show_leaderboard_during_contest,
});
router.push(`/admin/contests/${contest.id}/problems`);
} catch (err) {
@@ -114,19 +116,36 @@ export default function NewContestPage() {
-
-
- setFormData({ ...formData, is_active: e.target.checked })
- }
- className="w-4 h-4"
- />
-
+
diff --git a/frontend/src/app/leaderboard/[contestId]/page.tsx b/frontend/src/app/leaderboard/[contestId]/page.tsx
index 4c29547..69a8342 100644
--- a/frontend/src/app/leaderboard/[contestId]/page.tsx
+++ b/frontend/src/app/leaderboard/[contestId]/page.tsx
@@ -22,106 +22,149 @@ import {
} from "@/components/ui/table";
import {
Trophy,
- Medal,
ArrowLeft,
RefreshCw,
Clock,
Users,
Lock,
User,
+ Crown,
+ Target,
+ Zap,
} from "lucide-react";
+import { GlowOrbs, GlitchText } from "@/components/decorative";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
import type { Leaderboard, LeaderboardEntry } from "@/types";
-// Podium component for top 3
-function Podium({ entries }: { entries: LeaderboardEntry[] }) {
+// Rank display component
+function RankBadge({ rank }: { rank: number }) {
+ if (rank === 1) {
+ return (
+
+
+
+ );
+ }
+ if (rank === 2) {
+ return (
+
+ 2
+
+ );
+ }
+ if (rank === 3) {
+ return (
+
+ 3
+
+ );
+ }
+ return (
+
{rank}
+ );
+}
+
+// Top players highlight
+function TopPlayersHighlight({ entries }: { entries: LeaderboardEntry[] }) {
const top3 = entries.slice(0, 3);
-
- // Reorder for podium display: 2nd, 1st, 3rd
- const podiumOrder = [
- top3[1], // 2nd place (left)
- top3[0], // 1st place (center)
- top3[2], // 3rd place (right)
- ].filter(Boolean);
-
- const podiumHeights = {
- 1: "h-32",
- 2: "h-24",
- 3: "h-20",
- };
-
- const podiumColors = {
- 1: "from-yellow-400 to-amber-500",
- 2: "from-slate-300 to-slate-400",
- 3: "from-amber-600 to-amber-700",
- };
-
- const medalColors = {
- 1: "text-yellow-500",
- 2: "text-slate-400",
- 3: "text-amber-600",
- };
-
if (top3.length === 0) return null;
+ const colors = [
+ { bg: "var(--color-neon-yellow)", glow: "var(--color-neon-yellow)" },
+ { bg: "var(--color-foreground)", glow: "transparent" },
+ { bg: "var(--color-neon-orange)", glow: "var(--color-neon-orange)" },
+ ];
+
return (
-
- {podiumOrder.map((entry, index) => {
- if (!entry) return null;
- const rank = entry.rank as 1 | 2 | 3;
- const displayIndex = index === 1 ? 0 : index === 0 ? 1 : 2;
-
- return (
-
+ {top3.map((entry, index) => (
+
+
- {/* Avatar */}
-
- {entry.avatar_url ? (
-
- ) : (
-
- )}
-
+ {/* Top accent line */}
+
- {/* Username */}
-
- {entry.username}
-
+
+
+ {/* Avatar with glow */}
+
+ {index === 0 && (
+
+ )}
+
+ {entry.avatar_url ? (
+

+ ) : (
+
+ )}
+
+
- {/* Score */}
-
- {entry.total_score} баллов
-
-
- {/* Podium stand */}
-
-
- {rank}
-
-
-
- );
- })}
+
+
+
+ #{entry.rank}
+
+ {index === 0 && (
+
+ )}
+
+
{entry.username}
+
+
+
+ {entry.total_score} pts
+
+
+
+ {entry.problems_solved}
+
+
+
+
+
+
+
+ ))}
);
}
@@ -169,20 +212,19 @@ export default function LeaderboardPage() {
if (isLoading) {
return (
-
-
-
-
- {[...Array(3)].map((_, i) => (
-
-
-
-
-
-
- ))}
+
+
+
+
+
+
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+
-
);
}
@@ -196,209 +238,246 @@ export default function LeaderboardPage() {
}
return (
-
- {/* Header */}
-
-
-
-
+
+ {/* Background */}
+
+
-
-
-
-
- Таблица лидеров
-
-
{leaderboard.contest_title}
-
-
-
- {lastUpdated && (
-
-
- Обновлено: {lastUpdated.toLocaleTimeString("ru-RU")}
-
- )}
-
);
}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 55c1009..d1b5985 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -141,6 +141,7 @@ class ApiClient {
start_time: string;
end_time: string;
is_active?: boolean;
+ show_leaderboard_during_contest?: boolean;
}): Promise
{
return this.request("/api/contests/", {
method: "POST",
@@ -156,6 +157,7 @@ class ApiClient {
start_time: string;
end_time: string;
is_active: boolean;
+ show_leaderboard_during_contest: boolean;
}>
): Promise {
return this.request(`/api/contests/${id}`, {
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 1d89936..cec0f31 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -19,6 +19,7 @@ export interface Contest {
start_time: string;
end_time: string;
is_active: boolean;
+ show_leaderboard_during_contest: boolean;
created_by: number | null;
created_at: string;
is_running: boolean;
@@ -34,6 +35,7 @@ export interface ContestListItem {
start_time: string;
end_time: string;
is_active: boolean;
+ show_leaderboard_during_contest: boolean;
is_running: boolean;
has_ended: boolean;
problems_count: number;