From 839967ccf121f43f08d1837925be0d6d7d0c1be3 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 6 Feb 2026 15:12:17 +0100 Subject: [PATCH 01/13] Small change --- src/app/leaderboards/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/leaderboards/page.tsx b/src/app/leaderboards/page.tsx index 74f43d1..360b4d2 100644 --- a/src/app/leaderboards/page.tsx +++ b/src/app/leaderboards/page.tsx @@ -86,12 +86,12 @@ export default function Leaderboards() { > - + Global - - Weekly + + This week From 1db017b0b769b024e57289c91a999469f6da11a5 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 6 Feb 2026 16:16:15 +0100 Subject: [PATCH 02/13] Proper leaderboards layout --- .../components/LeaderboardRow.tsx | 93 +++++++++++-------- src/app/leaderboards/page.tsx | 24 +++-- src/modules/leaderboard/actions.ts | 86 ++++++++++++----- src/modules/leaderboard/types.ts | 4 +- 4 files changed, 139 insertions(+), 68 deletions(-) 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 360b4d2..0a9c3fe 100644 --- a/src/app/leaderboards/page.tsx +++ b/src/app/leaderboards/page.tsx @@ -114,16 +114,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/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; } From b525ac542dce77be09e6d84d1ec8b66275205be3 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 6 Feb 2026 16:57:09 +0100 Subject: [PATCH 03/13] Toggle group on leaderboards --- src/app/leaderboards/page.tsx | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/app/leaderboards/page.tsx b/src/app/leaderboards/page.tsx index 0a9c3fe..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 ( @@ -97,15 +97,23 @@ export default function Leaderboards() {
- {categories.map((cat, i) => ( - - {cat} - - ))} + + + any% + + + 100% + + + + + + Assisted + + + Unassisted + +
From cea3d4a47370a9ecc277812d8de9923b1626f4ee Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 6 Feb 2026 17:42:04 +0100 Subject: [PATCH 04/13] Toggle group on runs --- src/app/runs/page.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/app/runs/page.tsx b/src/app/runs/page.tsx index 35a1c37..9f8b8d3 100644 --- a/src/app/runs/page.tsx +++ b/src/app/runs/page.tsx @@ -18,6 +18,7 @@ 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); @@ -55,13 +56,14 @@ export default function LiveRuns() { {/* Filters */}
- {["Any%", "100%", "Easy", "Hard"].map((cat) => ( - - {cat} - - ))} - - 🔥 PB Pace + + + any% + + + 100% + +
{/* Table */} From 00deda5df1a49a6eadff14c8fee16e412d4d6616 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 6 Feb 2026 17:46:44 +0100 Subject: [PATCH 05/13] Toggle group on problems --- src/app/problems/page.tsx | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) 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 */} From 5e412f1cd9dd6cc1321e6d29585ccaa9bdba0c2a Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 6 Feb 2026 18:20:41 +0100 Subject: [PATCH 06/13] devrun Theme --- package-lock.json | 36 ++++++- package.json | 4 +- src/app/run/components/theme/create-theme.ts | 105 +++++++++++++++++++ src/app/run/components/theme/theme.ts | 81 ++++++++++++++ src/app/run/page.tsx | 27 ++++- 5 files changed, 243 insertions(+), 10 deletions(-) create mode 100644 src/app/run/components/theme/create-theme.ts create mode 100644 src/app/run/components/theme/theme.ts diff --git a/package-lock.json b/package-lock.json index ea2cd87..07b64a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "dev-run", "version": "0.1.0", "dependencies": { + "@codemirror/lang-javascript": "^6.2.4", + "@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 +58,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 +112,21 @@ "@lezer/common": "^1.1.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/language": { "version": "6.12.1", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", @@ -764,6 +781,17 @@ "@lezer/common": "^1.3.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/lr": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", @@ -3182,9 +3210,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..20cbe7e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "start": "next start" }, "dependencies": { + "@codemirror/lang-javascript": "^6.2.4", + "@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 +58,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/run/components/theme/create-theme.ts b/src/app/run/components/theme/create-theme.ts new file mode 100644 index 0000000..661a704 --- /dev/null +++ b/src/app/run/components/theme/create-theme.ts @@ -0,0 +1,105 @@ +import { EditorView } from "@codemirror/view"; +import { Extension } from "@codemirror/state"; +import { + HighlightStyle, + TagStyle, + syntaxHighlighting, +} from "@codemirror/language"; + +interface Options { + /** + * Theme variant. Determines which styles CodeMirror will apply by default. + */ + variant: Variant; + + /** + * Settings to customize the look of the editor, like background, gutter, selection and others. + */ + settings: Settings; + + /** + * Syntax highlighting styles. + */ + styles: TagStyle[]; +} + +type Variant = "light" | "dark"; + +interface Settings { + /** + * Editor background. + */ + background: string; + + /** + * Default text color. + */ + foreground: string; + + /** + * Caret color. + */ + caret: string; + + /** + * Selection background. + */ + selection: string; + + /** + * Background of highlighted lines. + */ + lineHighlight: string; + + /** + * Gutter background. + */ + gutterBackground: string; + + /** + * Text color inside gutter. + */ + gutterForeground: string; +} + +const createTheme = ({ variant, settings, styles }: Options): Extension => { + const theme = EditorView.theme( + { + // eslint-disable-next-line @typescript-eslint/naming-convention + "&": { + backgroundColor: settings.background, + color: settings.foreground, + }, + ".cm-content": { + caretColor: settings.caret, + }, + ".cm-cursor, .cm-dropCursor": { + borderLeftColor: settings.caret, + }, + "&.cm-focused .cm-selectionBackgroundm .cm-selectionBackground, .cm-content ::selection": + { + backgroundColor: settings.selection, + }, + ".cm-activeLine": { + backgroundColor: settings.lineHighlight, + }, + ".cm-gutters": { + backgroundColor: settings.gutterBackground, + color: settings.gutterForeground, + }, + ".cm-activeLineGutter": { + backgroundColor: settings.lineHighlight, + }, + }, + { + dark: variant === "dark", + }, + ); + + const highlightStyle = HighlightStyle.define(styles); + const extension = [theme, syntaxHighlighting(highlightStyle)]; + + return extension; +}; + +export default createTheme; diff --git a/src/app/run/components/theme/theme.ts b/src/app/run/components/theme/theme.ts new file mode 100644 index 0000000..cf47f3c --- /dev/null +++ b/src/app/run/components/theme/theme.ts @@ -0,0 +1,81 @@ +import { tags as t } from "@lezer/highlight"; +import createTheme from "./create-theme"; + +// Original theme: thememirror (dracula) +export const devRun = createTheme({ + variant: "dark", + settings: { + background: "var(--card)", // stays app-integrated + foreground: "#e6e6eb", + caret: "var(--primary)", + selection: "rgba(120, 120, 150, 0.25)", + gutterBackground: "var(--card)", + gutterForeground: "rgb(120, 122, 128)", + lineHighlight: "rgba(255, 255, 255, 0.03)", + }, + styles: [ + // 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..2e97a8e 100644 --- a/src/app/run/page.tsx +++ b/src/app/run/page.tsx @@ -1,14 +1,13 @@ "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 RunChat, { ChatMessage } from "./components/RunChat"; -import { useState } from "react"; import { FileTree } from "./components/FileTree"; import { File } from "lucide-react"; +import { devRun } from "./components/theme/theme"; +import { javascript } from "@codemirror/lang-javascript"; export default function Run() { const runInfo: RunInfo = { @@ -58,8 +57,26 @@ export default function Run() { 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(); `} extensions={[ + javascript(), EditorState.transactionFilter.of((tr) => { if (tr.selection && tr.isUserEvent("select")) { return []; @@ -67,7 +84,7 @@ export default function Run() { return tr; }), ]} - theme={vscodeDark} + theme={devRun} onCreateEditor={(view) => { view.focus(); view.dispatch({ From daad7392a08f09f2f0fadc350adbb0533e761894 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 6 Feb 2026 18:26:44 +0100 Subject: [PATCH 07/13] More languages --- package-lock.json | 116 ++++++++++++++++++++++++++++++ package.json | 5 ++ src/app/run/components/Editor.tsx | 82 +++++++++++++++++++++ src/app/run/page.tsx | 42 +---------- 4 files changed, 205 insertions(+), 40 deletions(-) create mode 100644 src/app/run/components/Editor.tsx diff --git a/package-lock.json b/package-lock.json index 07b64a8..1825125 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,12 @@ "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", @@ -112,6 +117,29 @@ "@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", @@ -127,6 +155,39 @@ "@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", @@ -772,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", @@ -781,6 +853,17 @@ "@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", @@ -792,6 +875,17 @@ "@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", @@ -801,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", diff --git a/package.json b/package.json index 20cbe7e..ba2be76 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,12 @@ "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", diff --git a/src/app/run/components/Editor.tsx b/src/app/run/components/Editor.tsx new file mode 100644 index 0000000..5924d5e --- /dev/null +++ b/src/app/run/components/Editor.tsx @@ -0,0 +1,82 @@ +"use client"; + +import CodeMirror, { EditorState } from "@uiw/react-codemirror"; +import { devRun } from "./theme/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"; + +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(); + } +} + +export default function Editor() { + 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(); `} + extensions={[ + getLanguageExtension("javascript"), + EditorState.transactionFilter.of((tr) => { + if (tr.selection && tr.isUserEvent("select")) { + return []; + } + return tr; + }), + ]} + theme={devRun} + onCreateEditor={(view) => { + view.focus(); + view.dispatch({ + selection: { anchor: 1 }, + scrollIntoView: true, + }); + }} + /> + ); +} diff --git a/src/app/run/page.tsx b/src/app/run/page.tsx index 2e97a8e..2a0ea25 100644 --- a/src/app/run/page.tsx +++ b/src/app/run/page.tsx @@ -8,6 +8,7 @@ import { FileTree } from "./components/FileTree"; import { File } from "lucide-react"; import { devRun } from "./components/theme/theme"; import { javascript } from "@codemirror/lang-javascript"; +import Editor from "./components/Editor"; export default function Run() { const runInfo: RunInfo = { @@ -52,47 +53,8 @@ export default function Run() {
myfile.js
-
- 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(); `} - extensions={[ - javascript(), - EditorState.transactionFilter.of((tr) => { - if (tr.selection && tr.isUserEvent("select")) { - return []; - } - return tr; - }), - ]} - theme={devRun} - onCreateEditor={(view) => { - view.focus(); - view.dispatch({ - selection: { anchor: 1 }, - scrollIntoView: true, - }); - }} - /> +
From 3b2809ed85ec4137a2146cb2153ed02a8d314b0f Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 6 Feb 2026 23:45:42 +0100 Subject: [PATCH 08/13] Improved editor --- src/app/globals.css | 4 +- src/app/run/components/Editor.tsx | 94 +++++++++++++-- src/app/run/components/theme.ts | 119 +++++++++++++++++++ src/app/run/components/theme/create-theme.ts | 105 ---------------- src/app/run/components/theme/theme.ts | 81 ------------- src/app/run/page.tsx | 5 +- 6 files changed, 203 insertions(+), 205 deletions(-) create mode 100644 src/app/run/components/theme.ts delete mode 100644 src/app/run/components/theme/create-theme.ts delete mode 100644 src/app/run/components/theme/theme.ts 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/run/components/Editor.tsx b/src/app/run/components/Editor.tsx index 5924d5e..ce7b7a3 100644 --- a/src/app/run/components/Editor.tsx +++ b/src/app/run/components/Editor.tsx @@ -1,13 +1,14 @@ "use client"; -import CodeMirror, { EditorState } from "@uiw/react-codemirror"; -import { devRun } from "./theme/theme"; +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"; type Language = | "javascript" @@ -37,12 +38,17 @@ function getLanguageExtension(lang: Language) { } } -export default function Editor() { - return ( - ); `} +root.render();`.repeat(3); + +// Helper to split text into chunks of ~4 chars +function chunkText(text: string, size = 4) { + const chunks = []; + for (let i = 0; i < text.length; i += size) { + chunks.push(text.slice(i, i + size)); + } + return chunks; +} + +const chunks = chunkText(code, 4); + +const moves: Move[] = chunks.map((chunk, index) => ({ + latency: 1, // 100ms per chunk + cursor: index * 4, // approximate cursor position + changes: { + from: index * 4, + to: index * 4, + insert: chunk, + }, +})); + +export default function Editor() { + const [editorView, setEditorView] = useState(); + + useEffect(() => { + if (!editorView) 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 () => { + for (const move of moves) { + if (cancelled) break; + await executeMove(move); + } + })(); + + return () => { + cancelled = true; // stop future moves + }; + }, [editorView]); + + return ( + { @@ -72,10 +143,7 @@ root.render(); `} theme={devRun} onCreateEditor={(view) => { view.focus(); - view.dispatch({ - selection: { anchor: 1 }, - scrollIntoView: true, - }); + setEditorView(view); }} /> ); 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/components/theme/create-theme.ts b/src/app/run/components/theme/create-theme.ts deleted file mode 100644 index 661a704..0000000 --- a/src/app/run/components/theme/create-theme.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { EditorView } from "@codemirror/view"; -import { Extension } from "@codemirror/state"; -import { - HighlightStyle, - TagStyle, - syntaxHighlighting, -} from "@codemirror/language"; - -interface Options { - /** - * Theme variant. Determines which styles CodeMirror will apply by default. - */ - variant: Variant; - - /** - * Settings to customize the look of the editor, like background, gutter, selection and others. - */ - settings: Settings; - - /** - * Syntax highlighting styles. - */ - styles: TagStyle[]; -} - -type Variant = "light" | "dark"; - -interface Settings { - /** - * Editor background. - */ - background: string; - - /** - * Default text color. - */ - foreground: string; - - /** - * Caret color. - */ - caret: string; - - /** - * Selection background. - */ - selection: string; - - /** - * Background of highlighted lines. - */ - lineHighlight: string; - - /** - * Gutter background. - */ - gutterBackground: string; - - /** - * Text color inside gutter. - */ - gutterForeground: string; -} - -const createTheme = ({ variant, settings, styles }: Options): Extension => { - const theme = EditorView.theme( - { - // eslint-disable-next-line @typescript-eslint/naming-convention - "&": { - backgroundColor: settings.background, - color: settings.foreground, - }, - ".cm-content": { - caretColor: settings.caret, - }, - ".cm-cursor, .cm-dropCursor": { - borderLeftColor: settings.caret, - }, - "&.cm-focused .cm-selectionBackgroundm .cm-selectionBackground, .cm-content ::selection": - { - backgroundColor: settings.selection, - }, - ".cm-activeLine": { - backgroundColor: settings.lineHighlight, - }, - ".cm-gutters": { - backgroundColor: settings.gutterBackground, - color: settings.gutterForeground, - }, - ".cm-activeLineGutter": { - backgroundColor: settings.lineHighlight, - }, - }, - { - dark: variant === "dark", - }, - ); - - const highlightStyle = HighlightStyle.define(styles); - const extension = [theme, syntaxHighlighting(highlightStyle)]; - - return extension; -}; - -export default createTheme; diff --git a/src/app/run/components/theme/theme.ts b/src/app/run/components/theme/theme.ts deleted file mode 100644 index cf47f3c..0000000 --- a/src/app/run/components/theme/theme.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { tags as t } from "@lezer/highlight"; -import createTheme from "./create-theme"; - -// Original theme: thememirror (dracula) -export const devRun = createTheme({ - variant: "dark", - settings: { - background: "var(--card)", // stays app-integrated - foreground: "#e6e6eb", - caret: "var(--primary)", - selection: "rgba(120, 120, 150, 0.25)", - gutterBackground: "var(--card)", - gutterForeground: "rgb(120, 122, 128)", - lineHighlight: "rgba(255, 255, 255, 0.03)", - }, - styles: [ - // 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 2a0ea25..850d17a 100644 --- a/src/app/run/page.tsx +++ b/src/app/run/page.tsx @@ -1,13 +1,10 @@ "use client"; -import CodeMirror, { EditorState } from "@uiw/react-codemirror"; import { Card, CardContent } from "@/components/ui/card"; import RunTimer, { RunInfo } from "./components/RunTimer"; import RunChat, { ChatMessage } from "./components/RunChat"; import { FileTree } from "./components/FileTree"; import { File } from "lucide-react"; -import { devRun } from "./components/theme/theme"; -import { javascript } from "@codemirror/lang-javascript"; import Editor from "./components/Editor"; export default function Run() { @@ -53,7 +50,7 @@ export default function Run() {
myfile.js
-
+
From 3c7e964d1c6b1ea505c683330bd286ecdbf7f224 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 6 Feb 2026 23:50:32 +0100 Subject: [PATCH 09/13] Improved editor --- src/app/run/components/Editor.tsx | 50 +++++-------------------------- src/modules/live-run/actions.ts | 36 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/app/run/components/Editor.tsx b/src/app/run/components/Editor.tsx index ce7b7a3..7b28440 100644 --- a/src/app/run/components/Editor.tsx +++ b/src/app/run/components/Editor.tsx @@ -9,6 +9,8 @@ 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" @@ -48,51 +50,12 @@ type Move = { }; }; -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); - -// Helper to split text into chunks of ~4 chars -function chunkText(text: string, size = 4) { - const chunks = []; - for (let i = 0; i < text.length; i += size) { - chunks.push(text.slice(i, i + size)); - } - return chunks; -} - -const chunks = chunkText(code, 4); - -const moves: Move[] = chunks.map((chunk, index) => ({ - latency: 1, // 100ms per chunk - cursor: index * 4, // approximate cursor position - changes: { - from: index * 4, - to: index * 4, - insert: chunk, - }, -})); - export default function Editor() { const [editorView, setEditorView] = useState(); + const [moves] = useAction(getLiveRunMoves); useEffect(() => { - if (!editorView) return; + if (!editorView || !moves) return; let cancelled = false; @@ -115,6 +78,7 @@ export default function Editor() { }; (async () => { + if (!moves) return; for (const move of moves) { if (cancelled) break; await executeMove(move); @@ -122,9 +86,9 @@ export default function Editor() { })(); return () => { - cancelled = true; // stop future moves + cancelled = true; }; - }, [editorView]); + }, [editorView, moves]); return ( { }, ]; } + +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: 1, // 100ms per chunk + cursor: index * 4, // approximate cursor position + changes: { + from: index * 4, + to: index * 4, + insert: chunk, + }, + })); +} From 9cf69fde7de4f1233eccd9b3722295ad5d659846 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 7 Feb 2026 00:40:37 +0100 Subject: [PATCH 10/13] Improved structure --- src/app/run/components/RunTimer.tsx | 43 ++++++++++++++++++++--------- src/app/run/page.tsx | 15 ++++------ src/modules/live-run/actions.ts | 15 ++++++++-- src/modules/live-run/types.ts | 8 ++++++ 4 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/app/run/components/RunTimer.tsx b/src/app/run/components/RunTimer.tsx index 8a5c5d1..39f7612 100644 --- a/src/app/run/components/RunTimer.tsx +++ b/src/app/run/components/RunTimer.tsx @@ -1,23 +1,40 @@ import { Card, CardContent } from "@/components/ui/card"; +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 = () => { + const elapsedMs = Date.now() - runInfo.start; + + const totalSeconds = 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 = (totalSeconds % 60).toFixed(2); + + setTime( + `${totalSeconds / 3600 >= 1 ? hours + ":" : ""}${minutes}:${seconds}`, + ); + }; + + // 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/page.tsx b/src/app/run/page.tsx index 850d17a..3ab4740 100644 --- a/src/app/run/page.tsx +++ b/src/app/run/page.tsx @@ -1,20 +1,16 @@ "use client"; 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 { 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"; 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" }, @@ -59,7 +55,8 @@ export default function Run() { {/* Timer & Chat */}
- + {runInfo && } + {/* {runInfo && } */}
diff --git a/src/modules/live-run/actions.ts b/src/modules/live-run/actions.ts index 69c9944..01870cf 100644 --- a/src/modules/live-run/actions.ts +++ b/src/modules/live-run/actions.ts @@ -3,8 +3,7 @@ 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, Run } from "./types"; export async function getLiveRuns(): Promise { const supabase = await createClient(); @@ -51,6 +50,16 @@ export async function getLiveRuns(): Promise { ]; } +export async function getLiveRun(): Promise { + return { + 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'; @@ -77,7 +86,7 @@ root.render();`.repeat(3); } return chunks.map((chunk, index) => ({ - latency: 1, // 100ms per chunk + latency: 120, // 100ms per chunk cursor: index * 4, // approximate cursor position changes: { from: index * 4, diff --git a/src/modules/live-run/types.ts b/src/modules/live-run/types.ts index b29b054..894fc84 100644 --- a/src/modules/live-run/types.ts +++ b/src/modules/live-run/types.ts @@ -1,3 +1,11 @@ +export interface LiveRun { + username: string; + start: number; + runsCount: number; + problem: string; + category: "any%" | "100%"; +} + export interface Run { id: number; username: string; From 75042007b7088026ed341eecafb2377c31440412 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 7 Feb 2026 00:55:23 +0100 Subject: [PATCH 11/13] Single liverun type --- src/app/run/components/RunTimer.tsx | 17 ++--------- src/app/runs/components/LiveRunNow.tsx | 30 +++++++++---------- src/app/runs/page.tsx | 8 ++--- src/lib/time-format.ts | 15 ++++++++++ src/modules/live-run/actions.ts | 41 ++++++++++++-------------- src/modules/live-run/types.ts | 11 +------ 6 files changed, 54 insertions(+), 68 deletions(-) create mode 100644 src/lib/time-format.ts diff --git a/src/app/run/components/RunTimer.tsx b/src/app/run/components/RunTimer.tsx index 39f7612..14fd33b 100644 --- a/src/app/run/components/RunTimer.tsx +++ b/src/app/run/components/RunTimer.tsx @@ -1,4 +1,5 @@ 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"; @@ -7,21 +8,7 @@ export default function RunTimer({ runInfo }: { runInfo: LiveRun }) { useEffect(() => { // Update function calculates elapsed time - const updateTime = () => { - const elapsedMs = Date.now() - runInfo.start; - - const totalSeconds = 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 = (totalSeconds % 60).toFixed(2); - - setTime( - `${totalSeconds / 3600 >= 1 ? hours + ":" : ""}${minutes}:${seconds}`, - ); - }; + const updateTime = () => setTime(formatTime(Date.now() - runInfo.start)); // Update immediately and then every second updateTime(); 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 9f8b8d3..9ceebb6 100644 --- a/src/app/runs/page.tsx +++ b/src/app/runs/page.tsx @@ -24,11 +24,7 @@ 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 (
@@ -82,7 +78,7 @@ export default function LiveRuns() { {sortedRuns.map((run, index) => ( - + ))} 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/live-run/actions.ts b/src/modules/live-run/actions.ts index 01870cf..9b4ebd2 100644 --- a/src/modules/live-run/actions.ts +++ b/src/modules/live-run/actions.ts @@ -3,55 +3,52 @@ import { createClient } from "@/lib/supabase/server"; import { getSession, getSessionRedirect } from "../account/core"; import * as Core from "./core"; -import { LiveRun, Run } from "./types"; +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", diff --git a/src/modules/live-run/types.ts b/src/modules/live-run/types.ts index 894fc84..97154f4 100644 --- a/src/modules/live-run/types.ts +++ b/src/modules/live-run/types.ts @@ -1,17 +1,8 @@ export interface LiveRun { + id: string; username: string; start: number; runsCount: number; problem: string; category: "any%" | "100%"; } - -export interface Run { - id: number; - username: string; - problem: string; - category: string; - time: string; - pace: string; - status: string; -} From 3e22d2da8a6d067d75c520a89d493fe8b830966f Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 7 Feb 2026 01:06:25 +0100 Subject: [PATCH 12/13] Better run layout --- src/app/run/components/RunTimer.tsx | 4 +- src/app/run/page.tsx | 79 +++++++++++++++++------------ 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/src/app/run/components/RunTimer.tsx b/src/app/run/components/RunTimer.tsx index 14fd33b..693256f 100644 --- a/src/app/run/components/RunTimer.tsx +++ b/src/app/run/components/RunTimer.tsx @@ -18,9 +18,9 @@ export default function RunTimer({ runInfo }: { runInfo: LiveRun }) { }, [runInfo.start]); return ( - +
- +

{time}

diff --git a/src/app/run/page.tsx b/src/app/run/page.tsx index 3ab4740..4b150d8 100644 --- a/src/app/run/page.tsx +++ b/src/app/run/page.tsx @@ -8,6 +8,7 @@ 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"; export default function Run() { const [runInfo] = useAction(getLiveRun); @@ -20,43 +21,57 @@ export default function Run() { return (
- {/* Editor */} - - - {/* File tree */} -
- +
+
+
+
- {/* Code */} -
-
- myfile.js -
-
- -
+
+
- - + +
+ {runInfo && } +
+
+ + {/* Editor */} + + + {/* File tree */} +
+ +
+ + {/* Code */} +
+
+ myfile.js +
+
+ +
+
+
+
+
{/* Timer & Chat */} -
- {runInfo && } - {/* {runInfo && } */} +
From 71b966088b10beb121d32a8a31667f76bf72fcbc Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 7 Feb 2026 01:31:47 +0100 Subject: [PATCH 13/13] Improved run layout --- src/app/problems/components/ProblemCard.tsx | 48 ++++++++++++--------- src/app/run/page.tsx | 13 +++--- src/components/app/ProfileCard.tsx | 37 ++++++++++++++++ 3 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 src/components/app/ProfileCard.tsx 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/run/page.tsx b/src/app/run/page.tsx index 4b150d8..ee44840 100644 --- a/src/app/run/page.tsx +++ b/src/app/run/page.tsx @@ -9,6 +9,7 @@ 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] = useAction(getLiveRun); @@ -23,17 +24,17 @@ export default function Run() {
-
- +
+ +
+ +
+ {runInfo && }
- -
- {runInfo && } -
{/* Editor */} 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 +
+
+ + ); +}