diff --git a/package-lock.json b/package-lock.json
index ea2cd87..1825125 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,13 @@
"name": "dev-run",
"version": "0.1.0",
"dependencies": {
+ "@codemirror/lang-go": "^6.0.1",
+ "@codemirror/lang-java": "^6.0.2",
+ "@codemirror/lang-javascript": "^6.2.4",
+ "@codemirror/lang-json": "^6.0.2",
+ "@codemirror/lang-python": "^6.2.1",
+ "@codemirror/lang-rust": "^6.0.2",
+ "@lezer/highlight": "^1.2.3",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
@@ -56,7 +63,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
- "@types/node": "^20.19.30",
+ "@types/node": "^20.19.32",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
@@ -110,6 +117,77 @@
"@lezer/common": "^1.1.0"
}
},
+ "node_modules/@codemirror/lang-go": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz",
+ "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.6.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/go": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-java": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz",
+ "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/java": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-javascript": {
+ "version": "6.2.4",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
+ "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.6.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/javascript": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-json": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
+ "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/json": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-python": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz",
+ "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.3.2",
+ "@codemirror/language": "^6.8.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/common": "^1.2.1",
+ "@lezer/python": "^1.1.4"
+ }
+ },
+ "node_modules/@codemirror/lang-rust": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz",
+ "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/rust": "^1.0.0"
+ }
+ },
"node_modules/@codemirror/language": {
"version": "6.12.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
@@ -755,6 +833,17 @@
"integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==",
"license": "MIT"
},
+ "node_modules/@lezer/go": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz",
+ "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.3.0"
+ }
+ },
"node_modules/@lezer/highlight": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
@@ -764,6 +853,39 @@
"@lezer/common": "^1.3.0"
}
},
+ "node_modules/@lezer/java": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz",
+ "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/javascript": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
+ "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.1.3",
+ "@lezer/lr": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/json": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
+ "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
"node_modules/@lezer/lr": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz",
@@ -773,6 +895,28 @@
"@lezer/common": "^1.0.0"
}
},
+ "node_modules/@lezer/python": {
+ "version": "1.1.18",
+ "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz",
+ "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/rust": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz",
+ "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
@@ -3182,9 +3326,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "20.19.30",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
- "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
+ "version": "20.19.32",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz",
+ "integrity": "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
diff --git a/package.json b/package.json
index e721d45..ba2be76 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,13 @@
"start": "next start"
},
"dependencies": {
+ "@codemirror/lang-go": "^6.0.1",
+ "@codemirror/lang-java": "^6.0.2",
+ "@codemirror/lang-javascript": "^6.2.4",
+ "@codemirror/lang-json": "^6.0.2",
+ "@codemirror/lang-python": "^6.2.1",
+ "@codemirror/lang-rust": "^6.0.2",
+ "@lezer/highlight": "^1.2.3",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
@@ -56,7 +63,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
- "@types/node": "^20.19.30",
+ "@types/node": "^20.19.32",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
diff --git a/src/app/globals.css b/src/app/globals.css
index 9b4f79b..efe06b6 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -314,5 +314,5 @@
}
.cm-editor {
- height: 100%;;
-}
\ No newline at end of file
+ height: 100%;
+}
diff --git a/src/app/leaderboards/components/LeaderboardRow.tsx b/src/app/leaderboards/components/LeaderboardRow.tsx
index 06d8598..884bc1b 100644
--- a/src/app/leaderboards/components/LeaderboardRow.tsx
+++ b/src/app/leaderboards/components/LeaderboardRow.tsx
@@ -1,71 +1,88 @@
-import { Trophy } from "lucide-react";
+import { LeaderboardRank } from "@/modules/leaderboard/types";
+import { LinkIcon, Trophy } from "lucide-react";
+import Link from "next/link";
-interface LeaderboardRowProps {
- rank: number;
- username: string;
- pb: string;
- runsCount: number;
- category: string;
+const LeaderboardRow = ({
+ index = 0,
+ run,
+}: {
index?: number;
-}
-
-const LeaderboardRow = ({ rank, username, pb, runsCount, category, index = 0 }: LeaderboardRowProps) => {
+ run: LeaderboardRank;
+}) => {
const getRankStyle = (rank: number) => {
- if (rank === 1) return "rank-gold";
- if (rank === 2) return "rank-silver";
- if (rank === 3) return "rank-bronze";
+ if (run.rank === 1) return "rank-gold";
+ if (run.rank === 2) return "rank-silver";
+ if (run.rank === 3) return "rank-bronze";
return "bg-secondary text-muted-foreground";
};
const getRankGlow = (rank: number) => {
- if (rank === 1) return "glow-gold";
- if (rank === 2) return "glow-silver";
- if (rank === 3) return "glow-bronze";
+ if (run.rank === 1) return "glow-gold";
+ if (run.rank === 2) return "glow-silver";
+ if (run.rank === 3) return "glow-bronze";
return "";
};
- const isTopThree = rank <= 3;
+ const isTopThree = run.rank <= 3;
return (
-
{/* Rank */}
-
-
- {rank <= 3 ? : rank}
+
+
+ {isTopThree ? : run.rank}
|
{/* Username */}
-
+ |
-
- {username.charAt(0).toUpperCase()}
-
-
- {username}
+
+ {run.username}
|
- {/* PB */}
-
- {pb}
- |
-
- {/* Runs */}
-
- {runsCount}
+ {/* Time */}
+ |
+
+ {run.pb}
+
|
{/* Category */}
- {category}
+ {run.category}
|
+
+ {/* Assisted */}
+
+
+ {run.assisted ? "Assisted" : "Unassisted"}
+
+ |
+
+ {/* Runs */}
+ {run.runsCount} |
+
+ {/* Problem */}
+
+
+ {run.problem}
+
+ |
|
);
};
diff --git a/src/app/leaderboards/page.tsx b/src/app/leaderboards/page.tsx
index 74f43d1..8487913 100644
--- a/src/app/leaderboards/page.tsx
+++ b/src/app/leaderboards/page.tsx
@@ -30,13 +30,13 @@ import {
getGlobalLeaderboard,
getWeeklyLeaderboard,
} from "@/modules/leaderboard/actions";
+import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export default function Leaderboards() {
const [global, globalLoaded, globalError] = useAction(getGlobalLeaderboard);
const [weekly, weeklyLoaded, weeklyError] = useAction(getWeeklyLeaderboard);
const [activeTab, setActiveTab] = useState<"global" | "weekly">("global");
- const categories = ["Any%", "100%", "Easy Only", "No DP", "Blind"];
const data = (activeTab === "global" ? global : weekly) || [];
return (
@@ -86,26 +86,34 @@ export default function Leaderboards() {
>
-
+
Global
-
- Weekly
+
+ This week
- {categories.map((cat, i) => (
-
- {cat}
-
- ))}
+
+
+ any%
+
+
+ 100%
+
+
+
+
+
+ Assisted
+
+
+ Unassisted
+
+
@@ -114,16 +122,28 @@ export default function Leaderboards() {
- Rank
- Runner
- PB
- Runs
- Category
+
+ Rank
+
+
+ Runner
+
+
+ Time
+
+
+ Category
+
+
+ Assisted
+
+ Runs
+ Problem
{data.map((entry, i) => (
-
+
))}
diff --git a/src/app/problems/components/ProblemCard.tsx b/src/app/problems/components/ProblemCard.tsx
index bd881e4..1e7f9b6 100644
--- a/src/app/problems/components/ProblemCard.tsx
+++ b/src/app/problems/components/ProblemCard.tsx
@@ -32,16 +32,17 @@ const ProblemCard = ({
return (
{/* Glow effect on hover */}
-
+
-
+ {/* Content */}
+
{/* Header */}
@@ -66,25 +67,30 @@ const ProblemCard = ({
))}
- {/* Stats */}
-
- {bestTime && (
-
-
- {bestTime}
-
- )}
-
-
- {attempts} runs
-
-
+ {/* Spacer */}
+
- {/* Quick run button - appears on hover */}
-
-
+ {/* Stats + Action */}
+
+
+ {bestTime && (
+
+
+ {bestTime}
+
+ )}
+
+
+ {attempts} runs
+
+
+
+ {/* Quick run button */}
+
+
+
diff --git a/src/app/problems/page.tsx b/src/app/problems/page.tsx
index 1992627..02cb1ce 100644
--- a/src/app/problems/page.tsx
+++ b/src/app/problems/page.tsx
@@ -16,7 +16,6 @@ export default function Problems() {
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
- const categories = ["All", "Arrays", "Trees", "Graphs", "DP", "Strings"];
const difficulties = ["All", "Easy", "Medium", "Hard"];
return (
@@ -40,28 +39,24 @@ export default function Problems() {
{/* Category Filter */}
- {categories.map((cat, i) => (
-
- {cat}
-
- ))}
+
+ {["Arrays", "Trees", "Graphs", "DP", "Strings"].map((f) => (
+
+ {f}
+
+ ))}
+
{/* Difficulty Filter */}
-
- {difficulties.map((diff, i) => (
-
- ))}
+
+
+ {["Easy", "Medium", "Hard"].map((f) => (
+
+ {f}
+
+ ))}
+
{/* View Toggle */}
diff --git a/src/app/run/components/Editor.tsx b/src/app/run/components/Editor.tsx
new file mode 100644
index 0000000..7b28440
--- /dev/null
+++ b/src/app/run/components/Editor.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import CodeMirror, { EditorState, EditorView } from "@uiw/react-codemirror";
+import { devRun } from "./theme";
+import { javascript } from "@codemirror/lang-javascript";
+import { rust } from "@codemirror/lang-rust";
+import { go } from "@codemirror/lang-go";
+import { json } from "@codemirror/lang-json";
+import { python } from "@codemirror/lang-python";
+import { java } from "@codemirror/lang-java";
+import { useEffect, useState } from "react";
+import useAction from "@/hook/use-action";
+import { getLiveRunMoves } from "@/modules/live-run/actions";
+
+type Language =
+ | "javascript"
+ | "typescript"
+ | "rust"
+ | "go"
+ | "python"
+ | "java"
+ | "json";
+
+function getLanguageExtension(lang: Language) {
+ switch (lang) {
+ case "javascript":
+ return javascript();
+ case "rust":
+ return rust();
+ case "go":
+ return go();
+ case "python":
+ return python();
+ case "java":
+ return java();
+ case "json":
+ return json();
+ default:
+ return javascript();
+ }
+}
+
+type Move = {
+ latency: number;
+ cursor: number;
+ changes: {
+ from: number;
+ to: number;
+ insert: string;
+ };
+};
+
+export default function Editor() {
+ const [editorView, setEditorView] = useState
();
+ const [moves] = useAction(getLiveRunMoves);
+
+ useEffect(() => {
+ if (!editorView || !moves) return;
+
+ let cancelled = false;
+
+ const executeMove = async (move: Move) => {
+ return new Promise((resolve) => {
+ const timeout = setTimeout(() => {
+ if (!editorView || cancelled) return resolve();
+
+ editorView.dispatch({
+ selection: { anchor: move.cursor },
+ scrollIntoView: true,
+ changes: move.changes,
+ });
+
+ resolve();
+ }, move.latency);
+
+ return () => clearTimeout(timeout);
+ });
+ };
+
+ (async () => {
+ if (!moves) return;
+ for (const move of moves) {
+ if (cancelled) break;
+ await executeMove(move);
+ }
+ })();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [editorView, moves]);
+
+ return (
+ {
+ if (tr.selection && tr.isUserEvent("select")) {
+ return [];
+ }
+ return tr;
+ }),
+ ]}
+ theme={devRun}
+ onCreateEditor={(view) => {
+ view.focus();
+ setEditorView(view);
+ }}
+ />
+ );
+}
diff --git a/src/app/run/components/RunTimer.tsx b/src/app/run/components/RunTimer.tsx
index 8a5c5d1..693256f 100644
--- a/src/app/run/components/RunTimer.tsx
+++ b/src/app/run/components/RunTimer.tsx
@@ -1,23 +1,27 @@
import { Card, CardContent } from "@/components/ui/card";
+import formatTime from "@/lib/time-format";
+import { LiveRun } from "@/modules/live-run/types";
+import { useEffect, useState } from "react";
-export type RunInfo = {
- category: "any%" | "100%";
- problems: number;
- currentAttempt: number;
- pbTime: string;
- worldRecord: string;
-};
+export default function RunTimer({ runInfo }: { runInfo: LiveRun }) {
+ const [time, setTime] = useState("00:00:00");
+
+ useEffect(() => {
+ // Update function calculates elapsed time
+ const updateTime = () => setTime(formatTime(Date.now() - runInfo.start));
+
+ // Update immediately and then every second
+ updateTime();
+ const interval = setInterval(updateTime, 50);
+
+ return () => clearInterval(interval); // cleanup on unmount
+ }, [runInfo.start]);
-export default function RunTimer({ runInfo }: { runInfo: RunInfo }) {
return (
-
+
-
- 1:52.34
-
- PB: {runInfo.pbTime} | Diff:{" "}
- +0:12
-
+
+ {time}
);
diff --git a/src/app/run/components/theme.ts b/src/app/run/components/theme.ts
new file mode 100644
index 0000000..745b9d4
--- /dev/null
+++ b/src/app/run/components/theme.ts
@@ -0,0 +1,119 @@
+import { EditorView } from "@codemirror/view";
+import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
+import { tags as t } from "@lezer/highlight";
+
+/**
+ * devRun — direct CodeMirror 6 theme
+ * Based on thememirror (dracula), app-integrated
+ */
+export const devRun = [
+ EditorView.theme(
+ {
+ "&": {
+ backgroundColor: "var(--card)",
+ color: "#e6e6eb",
+ },
+
+ ".cm-content": {
+ caretColor: "var(--primary)",
+ },
+
+ ".cm-cursor, .cm-dropCursor": {
+ borderLeftColor: "var(--primary)",
+ },
+
+ "&.cm-focused .cm-selectionBackground, .cm-content ::selection": {
+ backgroundColor: "rgba(120, 120, 150, 0.25)",
+ },
+
+ ".cm-activeLine": {
+ backgroundColor: "rgba(255, 255, 255, 0.03)",
+ },
+
+ ".cm-gutters": {
+ backgroundColor: "var(--card)",
+ color: "rgb(120, 122, 128)",
+ border: "none",
+ },
+
+ ".cm-activeLineGutter": {
+ backgroundColor: "rgba(255, 255, 255, 0.03)",
+ },
+
+ // - Scrollbar -
+ ".cm-scroller": {
+ scrollbarWidth: "thin",
+ scrollbarColor: "rgba(201, 133, 255, 0.3) transparent",
+ },
+ },
+ { dark: true },
+ ),
+
+ syntaxHighlighting(
+ HighlightStyle.define([
+ // Comments — low priority, low contrast
+ {
+ tag: t.comment,
+ color: "#5f6a8a",
+ fontStyle: "italic",
+ },
+
+ // Strings & literals — data, not logic
+ {
+ tag: [t.string, t.special(t.brace)],
+ color: "#f4e58c",
+ },
+
+ // Numbers / booleans — benchmarks & truth
+ {
+ tag: [t.number, t.bool, t.null],
+ color: "#c792ea",
+ fontWeight: "500",
+ },
+
+ // Keywords & operators — decisions & flow
+ {
+ tag: [t.keyword, t.operator],
+ color: "#ff5fa2",
+ fontWeight: "600",
+ },
+
+ // Types & definitions — structure
+ {
+ tag: [t.definitionKeyword, t.typeName],
+ color: "#6be6ff",
+ },
+
+ // Type definitions — neutral, readable
+ {
+ tag: t.definition(t.typeName),
+ color: "#e6e6eb",
+ },
+
+ // Functions, classes, props — execution paths
+ {
+ tag: [
+ t.className,
+ t.definition(t.propertyName),
+ t.function(t.variableName),
+ t.attributeName,
+ ],
+ color: "#4dff9f",
+ fontWeight: "500",
+ },
+
+ // Variables — slightly muted, context-aware
+ {
+ tag: t.variableName,
+ color: "#cfd3dc",
+ },
+
+ // Invalid / errors — instant attention
+ {
+ tag: t.invalid,
+ color: "#ff5555",
+ fontWeight: "700",
+ },
+ ]),
+ ),
+];
diff --git a/src/app/run/page.tsx b/src/app/run/page.tsx
index 2e86e67..ee44840 100644
--- a/src/app/run/page.tsx
+++ b/src/app/run/page.tsx
@@ -1,23 +1,18 @@
"use client";
-import CodeMirror, { EditorState } from "@uiw/react-codemirror";
-import { vscodeDark } from "@uiw/codemirror-theme-vscode";
-
import { Card, CardContent } from "@/components/ui/card";
-import RunTimer, { RunInfo } from "./components/RunTimer";
+import RunTimer from "./components/RunTimer";
import RunChat, { ChatMessage } from "./components/RunChat";
-import { useState } from "react";
import { FileTree } from "./components/FileTree";
import { File } from "lucide-react";
+import Editor from "./components/Editor";
+import useAction from "@/hook/use-action";
+import { getLiveRun } from "@/modules/live-run/actions";
+import ProblemCard from "../problems/components/ProblemCard";
+import ProfileCard from "@/components/app/ProfileCard";
export default function Run() {
- const runInfo: RunInfo = {
- category: "any%",
- problems: 5,
- pbTime: "4:35",
- worldRecord: "3:58",
- currentAttempt: 47,
- };
+ const [runInfo] = useAction(getLiveRun);
const chatMessages: ChatMessage[] = [
{ user: "Alice", message: "Nice split!", time: "1:53" },
@@ -27,63 +22,57 @@ export default function Run() {
return (
- {/* Editor */}
-
-
- {/* File tree */}
-
-
+
+
+
- {/* Code */}
-
-
+
+ {runInfo && }
+
-
-
{
- if (tr.selection && tr.isUserEvent("select")) {
- return [];
- }
- return tr;
- }),
- ]}
- theme={vscodeDark}
- onCreateEditor={(view) => {
- view.focus();
- view.dispatch({
- selection: { anchor: 1 },
- scrollIntoView: true,
- });
+
+
+
+ {/* Editor */}
+
+
+ {/* File tree */}
+
+
-
-
-
+
+ {/* Code */}
+
+
+
+
{/* Timer & Chat */}
-
diff --git a/src/app/runs/components/LiveRunNow.tsx b/src/app/runs/components/LiveRunNow.tsx
index 13e9919..6790689 100644
--- a/src/app/runs/components/LiveRunNow.tsx
+++ b/src/app/runs/components/LiveRunNow.tsx
@@ -5,18 +5,18 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { TableRow, TableCell } from "@/components/ui/table";
import { cn } from "@/lib/utils";
+import { LiveRun } from "@/modules/live-run/types";
+import formatTime from "@/lib/time-format";
export default function LiveRunRow({
index,
- username,
- problem,
- category,
- time,
- pace,
- status,
-}: any) {
- const pacePositive = pace.startsWith("+");
- const isTop = status === "pb";
+ run,
+}: {
+ index: number;
+ run: LiveRun;
+}) {
+ const now = new Date().getTime() + 1000;
+ const isTop = false;
return (
- {isTop ? `🔥 ${username}` : username}
+ {isTop ? `🔥 ${run.username}` : run.username}
- {problem}
+ {run.problem}
- {category}
+ {run.category}
@@ -45,18 +45,18 @@ export default function LiveRunRow({
- {time}
+ {formatTime(now - run.start, false)}
- {pace}
+ 0
diff --git a/src/app/runs/page.tsx b/src/app/runs/page.tsx
index 35a1c37..9ceebb6 100644
--- a/src/app/runs/page.tsx
+++ b/src/app/runs/page.tsx
@@ -18,16 +18,13 @@ import LiveRunRow from "./components/LiveRunNow";
import StatCard from "@/components/app/StatCard";
import useAction from "@/hook/use-action";
import { getLiveRuns } from "@/modules/live-run/actions";
+import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export default function LiveRuns() {
const [liveRuns, liveRunsLoaded, liveRunsError] = useAction(getLiveRuns);
// Sort top runners (status === 'pb') to the top
- const sortedRuns = [...(liveRuns || [])].sort((a, b) => {
- if (a.status === "pb" && b.status !== "pb") return -1;
- if (a.status !== "pb" && b.status === "pb") return 1;
- return 0;
- });
+ const sortedRuns = [...(liveRuns || [])];
return (
@@ -55,13 +52,14 @@ export default function LiveRuns() {
{/* Filters */}
- {["Any%", "100%", "Easy", "Hard"].map((cat) => (
-
- {cat}
-
- ))}
-
- 🔥 PB Pace
+
+
+ any%
+
+
+ 100%
+
+
{/* Table */}
@@ -80,7 +78,7 @@ export default function LiveRuns() {
{sortedRuns.map((run, index) => (
-
+
))}
diff --git a/src/components/app/ProfileCard.tsx b/src/components/app/ProfileCard.tsx
new file mode 100644
index 0000000..b6e878a
--- /dev/null
+++ b/src/components/app/ProfileCard.tsx
@@ -0,0 +1,37 @@
+import { Badge } from "../ui/badge";
+import { Card, CardContent } from "../ui/card";
+
+export default function ProfileCard({
+ user,
+}: {
+ user: { username: string; rank: number };
+}) {
+ return (
+
+
+
+
+ {/* Avatar */}
+
+

+
+
+ #{user.rank}
+
+
+
+ {/* User Info */}
+
+
+ {user.username}
+
+ World Record Holder
+
+
+
+ );
+}
diff --git a/src/lib/time-format.ts b/src/lib/time-format.ts
new file mode 100644
index 0000000..a437040
--- /dev/null
+++ b/src/lib/time-format.ts
@@ -0,0 +1,15 @@
+export default function formatTime(elapsedMs: number, formatMs = true) {
+ const totalSeconds = formatMs
+ ? elapsedMs / 1000
+ : Math.floor(elapsedMs / 1000);
+ const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, "0");
+ const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(
+ 2,
+ "0",
+ );
+ const seconds = formatMs
+ ? (totalSeconds % 60).toFixed(2).padStart(2, "0")
+ : (totalSeconds % 60).toString().padStart(2, "0");
+
+ return `${totalSeconds / 3600 >= 1 ? hours + ":" : ""}${minutes}:${seconds}`;
+}
diff --git a/src/modules/leaderboard/actions.ts b/src/modules/leaderboard/actions.ts
index 34f7f0a..c62bab4 100644
--- a/src/modules/leaderboard/actions.ts
+++ b/src/modules/leaderboard/actions.ts
@@ -1,154 +1,194 @@
"use server";
import * as Core from "./core";
-import { Leaderboard } from "./types";
+import { LeaderboardRank } from "./types"; // updated interface
-export async function getGlobalLeaderboard(): Promise
{
+export async function getGlobalLeaderboard(): Promise {
return [
{
rank: 1,
username: "speedmaster_x",
pb: "2:34.21",
runsCount: 2341,
- category: "Any%",
+ problem: "Fix broken login flow",
+ category: "any%",
+ assisted: false,
},
{
rank: 2,
username: "algo_ninja",
pb: "2:38.54",
runsCount: 1876,
- category: "Any%",
+ problem: "Refactor legacy API endpoints",
+ category: "any%",
+ assisted: false,
},
{
rank: 3,
username: "clockwork",
pb: "2:41.02",
runsCount: 3102,
- category: "Any%",
+ problem: "Optimize image upload pipeline",
+ category: "any%",
+ assisted: false,
},
{
rank: 4,
username: "fastfingers99",
pb: "2:45.33",
runsCount: 987,
- category: "Any%",
+ problem: "Build a mini chat server",
+ category: "any%",
+ assisted: false,
},
{
rank: 5,
username: "recursion_queen",
pb: "2:47.89",
runsCount: 1543,
- category: "Any%",
+ problem: "Fix CSS layout bug in dashboard",
+ category: "any%",
+ assisted: false,
},
{
rank: 6,
username: "bitshifter",
pb: "2:52.11",
runsCount: 2109,
- category: "Any%",
+ problem: "Implement JWT authentication",
+ category: "any%",
+ assisted: false,
},
{
rank: 7,
username: "nullpointer",
pb: "2:54.67",
runsCount: 876,
- category: "Any%",
+ problem: "Create REST endpoint for user data",
+ category: "any%",
+ assisted: false,
},
{
rank: 8,
username: "stackattack",
pb: "2:58.23",
runsCount: 1234,
- category: "Any%",
+ problem: "Fix off-by-one bug in sorting function",
+ category: "any%",
+ assisted: false,
},
{
rank: 9,
username: "looplegend",
pb: "3:01.45",
runsCount: 654,
- category: "Any%",
+ problem: "Optimize SQL query for reports",
+ category: "any%",
+ assisted: false,
},
{
rank: 10,
username: "coderunner",
pb: "3:05.78",
runsCount: 1098,
- category: "Any%",
+ problem: "Add file upload validation",
+ category: "any%",
+ assisted: false,
},
];
}
-export async function getWeeklyLeaderboard(): Promise {
+export async function getWeeklyLeaderboard(): Promise {
return [
{
rank: 1,
username: "weekly_warrior",
pb: "2:51.33",
runsCount: 156,
- category: "Any%",
+ problem: "Build notifications API",
+ category: "any%",
+ assisted: false,
},
{
rank: 2,
username: "speedmaster_x",
pb: "2:53.12",
runsCount: 89,
- category: "Any%",
+ problem: "Build notifications API",
+ category: "any%",
+ assisted: false,
},
{
rank: 3,
username: "newbie_grinder",
pb: "2:58.67",
runsCount: 234,
- category: "Any%",
+ problem: "Build notifications API",
+ category: "any%",
+ assisted: false,
},
{
rank: 4,
username: "algo_ninja",
pb: "3:02.45",
runsCount: 67,
- category: "Any%",
+ problem: "Build notifications API",
+ category: "any%",
+ assisted: false,
},
{
rank: 5,
username: "fresh_runner",
pb: "3:05.89",
runsCount: 45,
- category: "Any%",
+ problem: "Build notifications API",
+ category: "any%",
+ assisted: false,
},
{
rank: 6,
username: "practice_makes",
pb: "3:08.12",
runsCount: 112,
- category: "Any%",
+ problem: "Build notifications API",
+ category: "any%",
+ assisted: false,
},
{
rank: 7,
username: "splitmaster",
pb: "3:12.34",
runsCount: 78,
- category: "Any%",
+ problem: "Build notifications API",
+ category: "any%",
+ assisted: false,
},
{
rank: 8,
username: "grindtime",
pb: "3:15.56",
runsCount: 92,
- category: "Any%",
+ problem: "Build notifications API",
+ category: "any%",
+ assisted: false,
},
{
rank: 9,
username: "pb_hunter",
pb: "3:18.90",
runsCount: 56,
- category: "Any%",
+ problem: "Build notifications API",
+ category: "any%",
+ assisted: false,
},
{
rank: 10,
username: "reset_king",
pb: "3:22.11",
runsCount: 187,
- category: "Any%",
+ problem: "Build notifications API",
+ category: "any%",
+ assisted: false,
},
];
}
diff --git a/src/modules/leaderboard/types.ts b/src/modules/leaderboard/types.ts
index 4f25bf1..0f1cc51 100644
--- a/src/modules/leaderboard/types.ts
+++ b/src/modules/leaderboard/types.ts
@@ -5,5 +5,7 @@ export interface LeaderboardRank {
username: string;
pb: string;
runsCount: number;
- category: string;
+ problem: string;
+ category: "any%" | "100%";
+ assisted: boolean;
}
diff --git a/src/modules/live-run/actions.ts b/src/modules/live-run/actions.ts
index daf80a1..9b4ebd2 100644
--- a/src/modules/live-run/actions.ts
+++ b/src/modules/live-run/actions.ts
@@ -3,50 +3,92 @@
import { createClient } from "@/lib/supabase/server";
import { getSession, getSessionRedirect } from "../account/core";
import * as Core from "./core";
-import { Run } from "./types";
-import { redirect } from "next/navigation";
+import { LiveRun } from "./types";
-export async function getLiveRuns(): Promise {
+export async function getLiveRuns(): Promise {
const supabase = await createClient();
const _ = await getSession(supabase);
return [
{
- id: 1,
+ id: "1",
username: "speedmaster_x",
problem: "Two Sum Dash",
- category: "Any%",
- time: "1:52.34",
- pace: "-0:03",
- status: "pb",
+ category: "any%",
+ runsCount: 1,
+ start: new Date().getTime(),
},
{
- id: 2,
+ id: "2",
username: "algo_ninja",
problem: "Binary Sprint",
- category: "Any%",
- time: "2:04.11",
- pace: "+0:05",
- status: "normal",
+ category: "any%",
+ runsCount: 1,
+ start: new Date().getTime(),
},
{
- id: 3,
+ id: "3",
username: "stackattack",
problem: "String Scramble",
- category: "Easy",
- time: "0:48.92",
- pace: "-0:01",
- status: "pb",
+ category: "any%",
+ runsCount: 1,
+ start: new Date().getTime(),
},
{
- id: 4,
+ id: "4",
username: "reset_king",
problem: "Linked List Rush",
- category: "Any%",
- time: "1:31.77",
- pace: "+0:12",
- status: "danger",
+ category: "any%",
+ runsCount: 1,
+ start: new Date().getTime(),
},
];
}
+
+export async function getLiveRun(): Promise {
+ return {
+ id: "0",
+ username: "selimaj",
+ start: new Date().getTime(),
+ problem: "solve",
+ category: "any%",
+ runsCount: 0,
+ };
+}
+
+export async function getLiveRunMoves() {
+ const code = `// Import the necessary React library features
+import { createRoot } from 'react-dom/client';
+
+// Define a functional component named "App"
+// Component names must start with a capital letter
+function App() {
+ // Components return JSX markup
+ return (
+ Hello, world!
+ );
+}
+
+// Target a DOM element in your HTML (e.g., )
+const domNode = document.getElementById('root');
+
+// Create a root and render your component into the DOM
+const root = createRoot(domNode);
+root.render();`.repeat(3);
+
+ const chunks = [];
+ for (let i = 0; i < code.length; i += 4) {
+ chunks.push(code.slice(i, i + 4));
+ }
+
+ return chunks.map((chunk, index) => ({
+ latency: 120, // 100ms per chunk
+ cursor: index * 4, // approximate cursor position
+ changes: {
+ from: index * 4,
+ to: index * 4,
+ insert: chunk,
+ },
+ }));
+}
diff --git a/src/modules/live-run/types.ts b/src/modules/live-run/types.ts
index b29b054..97154f4 100644
--- a/src/modules/live-run/types.ts
+++ b/src/modules/live-run/types.ts
@@ -1,9 +1,8 @@
-export interface Run {
- id: number;
+export interface LiveRun {
+ id: string;
username: string;
+ start: number;
+ runsCount: number;
problem: string;
- category: string;
- time: string;
- pace: string;
- status: string;
+ category: "any%" | "100%";
}