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 */} -
-
- myfile.js -
+
+ {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 */} +
+
+ myfile.js +
+
+ +
+
+ + +
{/* 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 */} +
+ Img + +
+ #{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%"; }