Skip to content

Commit 57da649

Browse files
authored
Merge pull request #5 from eWloYW8/master
feat: links, better log viewer, footer
2 parents e6dcb50 + fe81438 commit 57da649

File tree

7 files changed

+296
-92
lines changed

7 files changed

+296
-92
lines changed

app/(main)/layout.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,35 @@ function MainLayout({ children }: { children: React.ReactNode }) {
1010
<MainNav />
1111
<div className="flex w-full items-center gap-4 md:ml-auto md:gap-2 lg:gap-4">
1212
<div className="ml-auto flex-1 sm:flex-initial">
13-
{/* Future search bar can go here */}
1413
</div>
1514
<UserNav />
1615
</div>
1716
</header>
1817
<main className="flex flex-1 flex-col gap-4 p-4 md:gap-8 md:p-8">
1918
{children}
2019
</main>
20+
<footer className="mt-auto border-t py-4">
21+
<div className="container mx-auto text-center text-sm text-muted-foreground">
22+
Powered by{" "}
23+
<a
24+
href="https://github.com/ZJUSCT/CSOJ"
25+
target="_blank"
26+
rel="noopener noreferrer"
27+
className="font-medium text-primary underline-offset-4 hover:underline"
28+
>
29+
ZJUSCT/CSOJ
30+
</a>{" "}
31+
&{" "}
32+
<a
33+
href="https://github.com/ZJUSCT/CSOJ-WebUI"
34+
target="_blank"
35+
rel="noopener noreferrer"
36+
className="font-medium text-primary underline-offset-4 hover:underline"
37+
>
38+
ZJUSCT/CSOJ-WebUI
39+
</a>
40+
</div>
41+
</footer>
2142
</div>
2243
);
2344
}

components/layout/main-nav.tsx

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,67 @@
11
"use client";
22
import Link from "next/link";
33
import { usePathname } from "next/navigation";
4+
import useSWR from 'swr';
45
import { cn } from "@/lib/utils";
56
import { CodeXml } from "lucide-react";
7+
import api from '@/lib/api';
8+
import { LinkItem } from '@/lib/types';
9+
10+
const fetcher = (url: string) => api.get(url).then(res => res.data.data);
611

712
export function MainNav({ className, ...props }: React.HTMLAttributes<HTMLElement>) {
813
const pathname = usePathname();
14+
const { data: dynamicLinks } = useSWR<LinkItem[]>('/links', fetcher, {
15+
revalidateOnFocus: false,
16+
});
917

10-
const routes = [
18+
const allRoutes = [
1119
{ href: "/contests", label: "Contests" },
1220
{ href: "/submissions", label: "Submissions" },
1321
{ href: "/profile", label: "Profile" },
22+
...(dynamicLinks?.map(link => ({ href: link.url, label: link.name })) || []),
1423
];
1524

1625
return (
1726
<nav className={cn("flex items-center space-x-4 lg:space-x-6", className)} {...props}>
18-
<Link href="/contests" className="flex items-center gap-2 font-semibold">
27+
<Link href="/contests" className="flex items-center gap-2 font-semibold">
1928
<CodeXml className="h-6 w-6" />
2029
<span className="">CSOJ</span>
2130
</Link>
22-
{routes.map((route) => (
23-
<Link
24-
key={route.href}
25-
href={route.href}
26-
className={cn(
27-
"text-sm font-medium transition-colors hover:text-primary",
28-
pathname.startsWith(route.href) ? "text-primary" : "text-muted-foreground"
29-
)}
30-
>
31-
{route.label}
32-
</Link>
33-
))}
31+
{allRoutes.map((route) => {
32+
const isExternal = route.href.startsWith("http");
33+
const isActive = !isExternal && pathname.startsWith(route.href);
34+
35+
if (isExternal) {
36+
return (
37+
<a
38+
key={route.href}
39+
href={route.href}
40+
target="_blank"
41+
rel="noopener noreferrer"
42+
className={cn(
43+
"text-sm font-medium transition-colors hover:text-primary",
44+
"text-muted-foreground"
45+
)}
46+
>
47+
{route.label}
48+
</a>
49+
);
50+
}
51+
52+
return (
53+
<Link
54+
key={route.href}
55+
href={route.href}
56+
className={cn(
57+
"text-sm font-medium transition-colors hover:text-primary",
58+
isActive ? "text-primary" : "text-muted-foreground"
59+
)}
60+
>
61+
{route.label}
62+
</Link>
63+
);
64+
})}
3465
</nav>
3566
);
3667
}

components/submissions/submission-log-viewer.tsx

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import { useState, useEffect, useRef } from 'react';
2+
import { useState, useEffect, useRef, useMemo } from 'react';
33
import useWebSocket, { ReadyState } from 'react-use-websocket';
44
import { useAuth } from '@/hooks/use-auth';
55
import { Problem, Submission, Container } from '@/lib/types';
@@ -8,25 +8,41 @@ import useSWR from 'swr';
88
import api from '@/lib/api';
99
import { Skeleton } from '../ui/skeleton';
1010

11+
interface LogMessage {
12+
stream: 'stdout' | 'stderr' | 'info' | 'error';
13+
data: string;
14+
}
15+
1116
// --- Sub-component for displaying static logs of finished containers ---
1217
const StaticLogViewer = ({ submissionId, containerId }: { submissionId: string, containerId: string }) => {
13-
// A simple text fetcher for SWR, as the log API returns plain text.
1418
const textFetcher = (url: string) => api.get(url, { responseType: 'text' }).then(res => res.data);
15-
1619
const { data: logText, error, isLoading } = useSWR(`/submissions/${submissionId}/containers/${containerId}/log`, textFetcher);
17-
1820
const logContainerRef = useRef<HTMLDivElement>(null);
1921

20-
// Scroll to bottom on initial load
22+
const messages: LogMessage[] = useMemo(() => {
23+
if (!logText) return [];
24+
return logText
25+
.split('\n')
26+
.filter((line: string) => line.trim() !== '')
27+
.map((line: string) => {
28+
try {
29+
return JSON.parse(line) as LogMessage;
30+
} catch (e) {
31+
console.error("Failed to parse log line as JSON:", line);
32+
return { stream: 'stdout', data: line };
33+
}
34+
});
35+
}, [logText]);
36+
2137
useEffect(() => {
2238
if (logContainerRef.current) {
2339
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
2440
}
25-
}, [logText]);
41+
}, [messages]);
2642

2743
return (
2844
<div className="relative">
29-
<div className="absolute top-2 right-2 text-xs font-semibold flex items-center gap-2 z-10">
45+
<div className="absolute top-2 right-6 text-xs font-semibold flex items-center gap-2 z-10">
3046
<span className="h-2 w-2 rounded-full bg-gray-400"></span>
3147
Finished
3248
</div>
@@ -36,18 +52,24 @@ const StaticLogViewer = ({ submissionId, containerId }: { submissionId: string,
3652
>
3753
{isLoading && <Skeleton className="h-full w-full" />}
3854
{error && <p className="text-red-400">Failed to load log.</p>}
39-
{logText && <pre className="whitespace-pre-wrap break-all">{logText}</pre>}
55+
{messages.length > 0 && messages.map((msg, index) => (
56+
<span key={index} className="whitespace-pre-wrap break-all">
57+
{msg.stream === 'stderr' || msg.stream === 'error' ? (
58+
<span className="text-red-400">{msg.data}</span>
59+
) : msg.stream === 'info' ? (
60+
<span className="text-blue-400">{msg.data}</span>
61+
) : (
62+
<span className="text-foreground">{msg.data}</span>
63+
)}
64+
</span>
65+
))}
4066
</div>
4167
</div>
4268
);
4369
};
4470

45-
// --- Sub-component for streaming real-time logs via WebSocket ---
46-
interface LogMessage {
47-
stream: 'stdout' | 'stderr' | 'info' | 'error';
48-
data: string;
49-
}
5071

72+
// --- Sub-component for streaming real-time logs via WebSocket ---
5173
const RealtimeLogViewer = ({ wsUrl, onStatusUpdate }: { wsUrl: string | null, onStatusUpdate: () => void }) => {
5274
const [messages, setMessages] = useState<LogMessage[]>([]);
5375
const logContainerRef = useRef<HTMLDivElement>(null);
@@ -92,7 +114,7 @@ const RealtimeLogViewer = ({ wsUrl, onStatusUpdate }: { wsUrl: string | null, on
92114

93115
return (
94116
<div className="relative">
95-
<div className="absolute top-2 right-2 text-xs font-semibold flex items-center gap-2 z-10">
117+
<div className="absolute top-2 right-6 text-xs font-semibold flex items-center gap-2 z-10">
96118
<span className={`h-2 w-2 rounded-full ${connectionStatus.color}`}></span>
97119
{connectionStatus.text}
98120
</div>
@@ -102,15 +124,15 @@ const RealtimeLogViewer = ({ wsUrl, onStatusUpdate }: { wsUrl: string | null, on
102124
>
103125
{messages.length === 0 && <p className="text-muted-foreground">Waiting for judge output...</p>}
104126
{messages.map((msg, index) => (
105-
<div key={index} className="whitespace-pre-wrap break-all">
127+
<span key={index} className="whitespace-pre-wrap break-all">
106128
{msg.stream === 'stderr' || msg.stream === 'error' ? (
107129
<span className="text-red-400">{msg.data}</span>
108130
) : msg.stream === 'info' ? (
109131
<span className="text-blue-400">{msg.data}</span>
110132
) : (
111133
<span className="text-foreground">{msg.data}</span>
112134
)}
113-
</div>
135+
</span>
114136
))}
115137
</div>
116138
</div>

0 commit comments

Comments
 (0)