Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions app/(main)/contests/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import { UserProfileCard } from '@/components/shared/user-profile-card';
import { getInitials } from '@/lib/utils';
import EchartsTrendChart from '@/components/charts/echarts-trend-chart';
import { AnnouncementsCard } from '@/components/contests/announcements-card';
import { UserScoreCard } from '@/components/contests/user-score-card';
import { DifficultyBadge } from '@/components/contests/difficulty-badge';
import UserScoreCard from '@/components/contests/user-score-card';
import { Search, List } from 'lucide-react';

const fetcher = (url: string) => api.get(url).then(res => res.data.data);

Expand Down Expand Up @@ -254,10 +256,10 @@ function ProblemCard({ problemId, index }: { problemId: string; index: number })
const t = useTranslations('contests');
const { data: problem, isLoading } = useSWR<Problem>(`/problems/${problemId}`, fetcher);

if (isLoading) return <Skeleton className="h-24 w-full" />;
if (isLoading) return <Skeleton className="h-28 w-full" />;

return (
<Link href={`/problems?id=${problemId}`} className="relative block overflow-hidden hover:shadow-lg transition-shadow duration-300">
<Link href={`/problems?id=${problemId}`} className="relative block overflow-hidden rounded-lg hover:shadow-lg transition-shadow duration-300">
<Card className="h-full">
<div className="relative z-10">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
Expand All @@ -266,6 +268,7 @@ function ProblemCard({ problemId, index }: { problemId: string; index: number })
</CardHeader>
<CardContent>
<div className="text-xs text-muted-foreground mb-4">{t('problemCard.id')}: {problemId}</div>
<DifficultyBadge level={problem?.level || ""} />
</CardContent>
</div>

Expand All @@ -275,7 +278,7 @@ function ProblemCard({ problemId, index }: { problemId: string; index: number })
absolute bottom-0 right-0 z-0
transform translate-x-1/5 translate-y-1/4
text-9xl font-extrabold
text-neutral-500/10 dark:text-white/5
text-gray-100 dark:text-zinc-700
pointer-events-none select-none
"
>
Expand All @@ -289,34 +292,52 @@ function ProblemCard({ problemId, index }: { problemId: string; index: number })
function ContestProblems({ contestId }: { contestId: string }) {
const t = useTranslations('contests');
const { data: contest, error, isLoading } = useSWR<Contest>(`/contests/${contestId}`, fetcher);

if (isLoading) return <Skeleton className="h-64 w-full" />;
if (error) return <div>{t('detail.loadFail')}</div>;
if (!contest) return <div>{t('detail.notFound')}</div>;

return (
<div className="space-y-6">
<Card>
<CardHeader><CardTitle>{t('description.title')}</CardTitle></CardHeader>
<CardHeader>
<div className="flex items-center space-x-2">
<Search className="w-5 h-5" />
<CardTitle className="font-bold">{t('description.title')}</CardTitle>
</div>
</CardHeader>
<CardContent>
<MarkdownViewer
content={contest.description}
<MarkdownViewer
content={contest.description}
assetContext="contest"
assetContextId={contest.id}
/>
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle>{t('problems.title')}</CardTitle>
<CardDescription>{contest.problem_ids.length > 0 ? t('problems.instruction') : t('problems.none')}</CardDescription>
<div className="flex items-center space-x-2">
<List className="w-5 h-5" />
<CardTitle className="font-bold">{t('problems.title')}</CardTitle>
</div>
<CardDescription>
{contest.problem_ids.length > 0
? t('problems.instruction')
: t('problems.none')}
</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{contest.problem_ids.map((problemId, i) => <ProblemCard key={problemId} problemId={problemId} index={i + 1} />)}
{contest.problem_ids.map((problemId, i) => (
<ProblemCard key={problemId} problemId={problemId} index={i + 1} />
))}
</CardContent>
</Card>
</div>
);
}


function ContestTrend({ contest }: { contest: Contest }) {
const t = useTranslations('contests');
const { data: trendData, error, isLoading } = useSWR<TrendEntry[]>(`/contests/${contest.id}/trend`, fetcher, { refreshInterval: 30000 });
Expand Down Expand Up @@ -536,8 +557,8 @@ function ContestDetailView({ contestId, view }: { contestId: string, view: strin
</div>

<div className="space-y-6 lg:sticky lg:top-20">
<AnnouncementsCard contestId={contestId} />
<UserScoreCard contestId={contestId} />
<AnnouncementsCard contestId={contestId} />
</div>
</div>
</div>
Expand Down
42 changes: 32 additions & 10 deletions app/(main)/problems/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Skeleton } from '@/components/ui/skeleton';
import { format } from 'date-fns';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { getScoreColor } from '@/lib/utils';
import { CopyButton } from '@/components/ui/shadcn-io/copy-button';
import { Suspense } from 'react';

const fetcher = (url: string) => api.get(url).then(res => res.data.data);

function UserSubmissionsForProblem({ problemId }: { problemId: string }) {
const router = useRouter();
const t = useTranslations('ProblemDetails');
const { data: allSubmissions, isLoading } = useSWR<Submission[]>('/submissions', fetcher);

Expand All @@ -32,23 +35,42 @@ function UserSubmissionsForProblem({ problemId }: { problemId: string }) {
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('submissions.id')}</TableHead>
<TableHead>{t('submissions.status')}</TableHead>
<TableHead>{t('submissions.score')}</TableHead>
<TableHead>{t('submissions.date')}</TableHead>
<TableHead className="text-right">{t('submissions.id')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{problemSubmissions.map(sub => (
<TableRow key={sub.id}>
<TableRow key={sub.id}
className="cursor-pointer transition-colors hover:bg-muted/50"
onClick={() => router.push(`/submissions?id=${sub.id}`)}
>
<TableCell><SubmissionStatusBadge status={sub.status} /></TableCell>
<TableCell>
<Link href={`/submissions?id=${sub.id}`} className="font-mono text-primary hover:underline">
{sub.id.substring(0, 8)}...
</Link>
<span
className="font-bold font-mono"
style={{
color: getScoreColor(sub.score ?? 0),
}}
>
{sub.score}
</span>
</TableCell>
<TableCell>{format(new Date(sub.CreatedAt), "MM/dd HH:mm:ss")}</TableCell>
<TableCell className="text-right font-mono text-sm text-muted-foreground">
<div className="flex items-center justify-end space-x-2">
<span className="mx-2">{sub.id.substring(0, 8)}</span>
<div
onClick={(e) => {
e.stopPropagation();
}}
>
<CopyButton content={sub.id} size="sm" />
</div>
</div>
</TableCell>
<TableCell><SubmissionStatusBadge status={sub.status} /></TableCell>
<TableCell>{sub.score}</TableCell>
<TableCell>{format(new Date(sub.CreatedAt), "Pp")}</TableCell>
</TableRow>
))}
</TableBody>
Expand Down Expand Up @@ -78,7 +100,7 @@ function ProblemDetails() {
if (!problem) return <div>{t('details.notFound')}</div>;

return (
<div className="grid gap-6 lg:grid-cols-2">
<div className="grid gap-6 lg:grid-cols-[6fr_4fr]">
<div className="space-y-6">
<Card>
<CardHeader>
Expand Down
65 changes: 22 additions & 43 deletions app/(main)/submissions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,15 @@ import { Separator } from '@/components/ui/separator';
import { useTranslations } from 'next-intl';
import { cn } from '@/lib/utils';
import { CopyButton } from "@/components/ui/shadcn-io/copy-button";
import { useRouter } from "next/navigation";
import { getScoreColor } from '@/lib/utils';
import MarkdownViewer from '@/components/shared/markdown-viewer';

const fetcher = (url: string) => api.get(url).then(res => res.data.data);

function getScoreColor(score: number): string {
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(n, max));
const s = clamp(score, 0, 100);

const startHue = 0;
const endHue = 130;
const hue = startHue + ((endHue - startHue) * s) / 100;

const saturation = 50;
const lightness = 50;
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}

function MySubmissionsList() {
const t = useTranslations('submissions');
const router = useRouter();

const { data: submissions, error, isLoading } = useSWR<Submission[]>('/submissions', fetcher, {
refreshInterval: 5000,
Expand Down Expand Up @@ -67,8 +57,7 @@ function MySubmissionsList() {
return (
<Card>
<CardHeader>
<CardTitle>{t('list.title')}</CardTitle>
<CardDescription>{t('list.description')}</CardDescription>
<CardTitle className="text-2xl font-bold">{t('list.title')}</CardTitle>
</CardHeader>
<CardContent>
<Table>
Expand Down Expand Up @@ -118,47 +107,37 @@ function MySubmissionsList() {
<TableRow
key={sub.id}
className={cn(
"transition-colors hover:bg-muted/50"
"cursor-pointer transition-colors hover:bg-muted/50"
)}
onClick={() => router.push(`/submissions?id=${sub.id}`)}
>
<TableCell>
<Link href={`/submissions?id=${sub.id}`} >
<SubmissionStatusBadge status={sub.status} />
</Link>
<SubmissionStatusBadge status={sub.status} />
</TableCell>
<TableCell>
<Link href={`/submissions?id=${sub.id}`} >
<span
className="font-semibold font-mono"
style={{
color: getScoreColor(sub.score ?? 0),
}}
>
{sub.score}
</span>
</Link>
<span
className="font-bold font-mono"
style={{
color: getScoreColor(sub.score ?? 0),
}}
>
{sub.score}
</span>
</TableCell>
<TableCell>
<Link href={`/problems?id=${sub.problem_id}`} >
<span className="text-primary hover:underline">{sub.problem_id}</span>
</Link>
{sub.problem_id}
</TableCell>
<TableCell>{sub.node}</TableCell>
<TableCell>
<Link href={`/submissions?id=${sub.id}`} >
{sub.node}
</Link>
{format(new Date(sub.CreatedAt), "MM/dd HH:mm:ss")}
</TableCell>
<TableCell>
<Link href={`/submissions?id=${sub.id}`} >
{format(new Date(sub.CreatedAt), "MM/dd HH:mm:ss")}
</Link>
</TableCell>
<TableCell className="text-right font-mono text-sm text-muted-foreground">
<div className="flex items-center justify-end space-x-2">
<span className="mx-2">{sub.id.substring(0, 8)}</span>
<div
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
}}
>
<CopyButton content={sub.id} size="sm" />
</div>
Expand All @@ -168,7 +147,7 @@ function MySubmissionsList() {
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center">
<TableCell colSpan={6} className="text-center">
{t('list.none')}
</TableCell>
</TableRow>
Expand Down
2 changes: 1 addition & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

@layer base {
:root {
--background: 0 0% 100%;
--background: 0 0% 99%;
--foreground: 222.2 84% 4.9%;

--card: 0 0% 100%;
Expand Down
4 changes: 2 additions & 2 deletions components/contests/announcements-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ export function AnnouncementsCard({ contestId }: { contestId: string }) {
)}
{error && <p className="text-sm text-destructive">{t('loadFail')}</p>}
{!isLoading && announcements && announcements.length > 0 ? (
<div className="space-y-6">
<div className="space-y-3">
{announcements.map((ann, index) => (
<div key={ann.id}>
<div className="space-y-1">
<h3 className="font-semibold">{ann.title}</h3>
<p className="text-xs text-muted-foreground" title={format(new Date(ann.created_at), 'Pp', { locale: locales[locale] || enUS })}>
{formatDistanceToNow(new Date(ann.created_at), { addSuffix: true, locale: locales[locale] || enUS })}
</p>
<div className="pt-2">
<div>
<MarkdownViewer content={ann.description} />
</div>
</div>
Expand Down
Loading