diff --git a/src/app/problems/components/ProblemCard.tsx b/src/app/problems/components/ProblemCard.tsx index 3ea94e4..8b0e4ff 100644 --- a/src/app/problems/components/ProblemCard.tsx +++ b/src/app/problems/components/ProblemCard.tsx @@ -97,7 +97,13 @@ const ProblemCard = ({ {/* Footer */} - + { + await fetch(`http://127.0.0.1:63780/start-run/${problem.id}`); + window.open("vscode://", "_blank"); + }} + > diff --git a/src/app/problems/page.tsx b/src/app/problems/page.tsx index 2abc43d..2810e66 100644 --- a/src/app/problems/page.tsx +++ b/src/app/problems/page.tsx @@ -14,8 +14,6 @@ import { getProblems } from "@/modules/problems/actions"; export default function Problems() { const [problems, problemsLoaded, problemsError] = useAction(getProblems); - console.log(problems); - const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); const difficulties = ["All", "Easy", "Medium", "Hard"]; diff --git a/src/app/run/components/Editor.tsx b/src/app/runs/[runId]/components/Editor.tsx similarity index 85% rename from src/app/run/components/Editor.tsx rename to src/app/runs/[runId]/components/Editor.tsx index 7b28440..dc9b865 100644 --- a/src/app/run/components/Editor.tsx +++ b/src/app/runs/[runId]/components/Editor.tsx @@ -9,8 +9,9 @@ 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 useAction, { useActionOnce } from "@/hook/use-action"; import { getLiveRunMoves } from "@/modules/live-run/actions"; +import { LiveRun, LiveRunMove } from "@/modules/live-run/types"; type Language = | "javascript" @@ -40,26 +41,19 @@ function getLanguageExtension(lang: Language) { } } -type Move = { - latency: number; - cursor: number; - changes: { - from: number; - to: number; - insert: string; - }; -}; - -export default function Editor() { +export default function Editor({ run }: { run: LiveRun | null | undefined }) { const [editorView, setEditorView] = useState(); - const [moves] = useAction(getLiveRunMoves); + const [moves] = useActionOnce( + async () => (run ? (await getLiveRunMoves(run.id)) || [] : []), + [run], + ); useEffect(() => { if (!editorView || !moves) return; let cancelled = false; - const executeMove = async (move: Move) => { + const executeMove = async (move: LiveRunMove) => { return new Promise((resolve) => { const timeout = setTimeout(() => { if (!editorView || cancelled) return resolve(); diff --git a/src/app/run/components/FileTree.tsx b/src/app/runs/[runId]/components/FileTree.tsx similarity index 100% rename from src/app/run/components/FileTree.tsx rename to src/app/runs/[runId]/components/FileTree.tsx diff --git a/src/app/run/components/ProfileCard.tsx b/src/app/runs/[runId]/components/ProfileCard.tsx similarity index 100% rename from src/app/run/components/ProfileCard.tsx rename to src/app/runs/[runId]/components/ProfileCard.tsx diff --git a/src/app/run/components/RunChat.tsx b/src/app/runs/[runId]/components/RunChat.tsx similarity index 100% rename from src/app/run/components/RunChat.tsx rename to src/app/runs/[runId]/components/RunChat.tsx diff --git a/src/app/run/components/RunTimer.tsx b/src/app/runs/[runId]/components/RunTimer.tsx similarity index 68% rename from src/app/run/components/RunTimer.tsx rename to src/app/runs/[runId]/components/RunTimer.tsx index 4f0748d..da520b8 100644 --- a/src/app/run/components/RunTimer.tsx +++ b/src/app/runs/[runId]/components/RunTimer.tsx @@ -1,28 +1,37 @@ import { Card, CardContent } from "@/components/ui/card"; import formatTime from "@/lib/time-format"; import { LiveRun } from "@/modules/live-run/types"; +import { Eye } from "lucide-react"; import { useEffect, useState } from "react"; -export default function RunTimer({ runInfo }: { runInfo: LiveRun }) { +export default function RunTimer({ run }: { run: LiveRun }) { const [time, setTime] = useState("00:00:00"); useEffect(() => { // Update function calculates elapsed time - const updateTime = () => setTime(formatTime(Date.now() - runInfo.start)); + const updateTime = () => setTime(formatTime(Date.now() - run.start)); // Update immediately and then every second updateTime(); const interval = setInterval(updateTime, 50); return () => clearInterval(interval); // cleanup on unmount - }, [runInfo.start]); + }, [run.start]); return ( {time} - {runInfo.category} + + + {run.category} + + + + {run.views} + + ); diff --git a/src/app/run/components/theme.ts b/src/app/runs/[runId]/components/theme.ts similarity index 100% rename from src/app/run/components/theme.ts rename to src/app/runs/[runId]/components/theme.ts diff --git a/src/app/run/page.tsx b/src/app/runs/[runId]/page.tsx similarity index 83% rename from src/app/run/page.tsx rename to src/app/runs/[runId]/page.tsx index 32312af..943d86f 100644 --- a/src/app/run/page.tsx +++ b/src/app/runs/[runId]/page.tsx @@ -6,13 +6,24 @@ 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 useAction, { useActionInterval } from "@/hook/use-action"; import { getLiveRun } from "@/modules/live-run/actions"; -import ProblemCard from "../problems/components/ProblemCard"; +import ProblemCard from "@/app/problems/components/ProblemCard"; import ProfileCard from "./components/ProfileCard"; +import { getProblem } from "@/modules/problems/actions"; +import { useParams } from "next/navigation"; export default function Run() { - const [runInfo] = useAction(getLiveRun); + const params = useParams<{ runId: string }>(); + + const [run] = useActionInterval(() => getLiveRun(params.runId), 1000, [ + params, + ]); + + const [problem] = useAction( + async () => (run?.id ? getProblem(run.problem) : null), + [run], + ); const chatMessages: ChatMessage[] = [ { user: "Alice", message: "Nice split!", time: "1:53" }, @@ -40,21 +51,11 @@ export default function Run() { - {runInfo && } + {run && } - + {problem && } @@ -85,7 +86,7 @@ export default function Run() { myfile.js - + diff --git a/src/app/runs/components/LiveRunRow.tsx b/src/app/runs/components/LiveRunRow.tsx index 6790689..08faad2 100644 --- a/src/app/runs/components/LiveRunRow.tsx +++ b/src/app/runs/components/LiveRunRow.tsx @@ -1,12 +1,15 @@ "use client"; -import { Eye } from "lucide-react"; 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"; +import useAction from "@/hook/use-action"; +import { getProblem } from "@/modules/problems/actions"; +import Link from "next/link"; +import { Eye, LinkIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; export default function LiveRunRow({ index, @@ -15,8 +18,10 @@ export default function LiveRunRow({ index: number; run: LiveRun; }) { + const [problem] = useAction(() => getProblem(run.problem)); + const now = new Date().getTime() + 1000; - const isTop = false; + const isTop = run.views > 5; return ( - + {/* Runner */} + {isTop ? `🔥 ${run.username}` : run.username} + {/* Problem + Category */} - - {run.problem} - - {run.category} - - + + + {problem?.title || run.problem} + + {/* Runtime */} {formatTime(now - run.start, false)} - - - 0 - + {/* Views */} + + {isTop ? {run.views} : run.views} - - - - Live - - + {/* Category */} + {run.category} - - - - Watch - + {/* Actions */} + + + + + Watch + + ); diff --git a/src/app/runs/page.tsx b/src/app/runs/page.tsx index 6af1023..51ec027 100644 --- a/src/app/runs/page.tsx +++ b/src/app/runs/page.tsx @@ -1,27 +1,20 @@ "use client"; -import { Zap, Users, Timer, TrendingUp, Eye } from "lucide-react"; - -import { Card, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import { Table, TableBody, TableHead, TableHeader, TableRow, - TableCell, } from "@/components/ui/table"; -import { cn } from "@/lib/utils"; import LiveRunRow from "./components/LiveRunRow"; -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); + const [liveRuns] = useAction(getLiveRuns); // Sort top runners (status === 'pb') to the top const sortedRuns = [...(liveRuns || [])]; @@ -33,30 +26,17 @@ export default function LiveRuns() { Live Runs - Watch runners compete in real time. Every second counts. + 42 live runs · Streaming in real time - {/* Stats */} - - - - - - - {/* Filters */} - - + + any% - + 100% @@ -69,10 +49,10 @@ export default function LiveRuns() { Runner Problem - Time - Pace - Status - + Time + Views + Category + diff --git a/src/hook/use-action.ts b/src/hook/use-action.ts index 0f7468a..984d771 100644 --- a/src/hook/use-action.ts +++ b/src/hook/use-action.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; export default function useAction( action: () => Promise, - dependencies = [], + dependencies: any[] = [], ): [T | null, boolean, Error | null] { const [value, setValue] = useState(null); const [loaded, setLoaded] = useState(false); @@ -17,3 +17,49 @@ export default function useAction( return [value, loaded, error]; } + +export function useActionOnce( + action: () => Promise, + dependencies: any[] = [], +): [T | null, boolean, Error | null] { + const [value, setValue] = useState(null); + const [loaded, setLoaded] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (loaded) return; + + action() + .then(setValue) + .catch(setError) + .finally(() => setLoaded(true)); + }, dependencies); + + return [value, loaded, error]; +} + +export function useActionInterval( + action: () => Promise, + interval: number, + dependencies: any[] = [], +): [T | null, boolean, Error | null] { + const [value, setValue] = useState(null); + const [loaded, setLoaded] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const f = () => + action() + .then(setValue) + .catch(setError) + .finally(() => setLoaded(true)); + + f(); + + const inv = setInterval(f, interval); + + return () => clearInterval(inv); + }, dependencies); + + return [value, loaded, error]; +} diff --git a/src/modules/live-run/actions.ts b/src/modules/live-run/actions.ts index 9b4ebd2..55b95a6 100644 --- a/src/modules/live-run/actions.ts +++ b/src/modules/live-run/actions.ts @@ -1,94 +1,16 @@ "use server"; -import { createClient } from "@/lib/supabase/server"; -import { getSession, getSessionRedirect } from "../account/core"; import * as Core from "./core"; import { LiveRun } from "./types"; export async function getLiveRuns(): Promise { - const supabase = await createClient(); - - const _ = await getSession(supabase); - - return [ - { - id: "1", - username: "speedmaster_x", - problem: "Two Sum Dash", - category: "any%", - runsCount: 1, - start: new Date().getTime(), - }, - { - id: "2", - username: "algo_ninja", - problem: "Binary Sprint", - category: "any%", - runsCount: 1, - start: new Date().getTime(), - }, - { - id: "3", - username: "stackattack", - problem: "String Scramble", - category: "any%", - runsCount: 1, - start: new Date().getTime(), - }, - { - id: "4", - username: "reset_king", - problem: "Linked List Rush", - category: "any%", - runsCount: 1, - start: new Date().getTime(), - }, - ]; + return Core.getLiveRuns(); } -export async function getLiveRun(): Promise { - return { - id: "0", - username: "selimaj", - start: new Date().getTime(), - problem: "solve", - category: "any%", - runsCount: 0, - }; +export async function getLiveRun(id: string): Promise { + return Core.getLiveRun(id); } -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, - }, - })); +export async function getLiveRunMoves(id: string) { + return Core.getLiveRunMoves(id); } diff --git a/src/modules/live-run/core.ts b/src/modules/live-run/core.ts index a59f71d..bef5c97 100644 --- a/src/modules/live-run/core.ts +++ b/src/modules/live-run/core.ts @@ -1,3 +1,40 @@ "use server"; import "server-only"; +import { LiveRun, LiveRunMove } from "./types"; + +const liveRuns: LiveRun[] = []; +const liveRunMoves: Record = {}; + +export async function getLiveRuns(): Promise { + return liveRuns; +} + +export async function getLiveRun(id: string): Promise { + return liveRuns.find((r) => r.id === id); +} + +export async function getLiveRunMoves( + id: string, +): Promise { + return liveRunMoves[id]; +} + +export async function addRun(run: LiveRun) { + if (!liveRuns.find((r) => r.id === run.id || r.username === run.username)) + throw "Duplicate run"; + + liveRuns.push(run); +} + +export async function addLiveRunMoves(id: string, moves: LiveRunMove[]) { + if (!liveRunMoves[id]) throw "Invalid run"; + + // Add the moves immediately + liveRunMoves[id] = [...liveRunMoves[id], ...moves]; + + // After 3 seconds, remove only the moves we just added + setTimeout(() => { + liveRunMoves[id] = liveRunMoves[id].filter((move) => !moves.includes(move)); + }, 3000); +} diff --git a/src/modules/live-run/types.ts b/src/modules/live-run/types.ts index 97154f4..9673d12 100644 --- a/src/modules/live-run/types.ts +++ b/src/modules/live-run/types.ts @@ -1,8 +1,19 @@ export interface LiveRun { id: string; username: string; + views: number; start: number; runsCount: number; problem: string; category: "any%" | "100%"; } + +export interface LiveRunMove { + latency: number; + cursor: number; + changes?: { + from: number; + to: number; + insert: string; + }; +} diff --git a/src/modules/problems/actions.ts b/src/modules/problems/actions.ts index a549c80..22eb133 100644 --- a/src/modules/problems/actions.ts +++ b/src/modules/problems/actions.ts @@ -6,3 +6,7 @@ import { Problem } from "./types"; export async function getProblems(): Promise { return await Core.getProblems(); } + +export async function getProblem(id: string): Promise { + return await Core.getProblem(id); +} diff --git a/src/modules/problems/core.ts b/src/modules/problems/core.ts index 0189644..a0c2346 100644 --- a/src/modules/problems/core.ts +++ b/src/modules/problems/core.ts @@ -1,7 +1,7 @@ "use server"; import "server-only"; -import { ProblemDefinition } from "./types"; +import { Problem, ProblemDefinition } from "./types"; const problems: ProblemDefinition[] = [ { @@ -20,7 +20,7 @@ const problems: ProblemDefinition[] = [ description: "Production build fails while development works. Fix the build.", language: "javascript", - difficulty: "easy", + difficulty: "hard", validate100: () => true, validateAny: () => true, }, @@ -29,13 +29,13 @@ const problems: ProblemDefinition[] = [ title: "Implement Missing API Endpoint", description: "The frontend expects `/api/me`. Implement it correctly.", language: "go", - difficulty: "easy", + difficulty: "medium", validate100: () => true, validateAny: () => true, }, ]; -export async function getProblems() { +export async function getProblems(): Promise { return problems.map((def) => ({ id: def.id, title: def.title, @@ -44,3 +44,17 @@ export async function getProblems() { language: def.language, })); } + +export async function getProblem(id: string): Promise { + const problem = problems.find((p) => p.id === id); + + if (!problem) return; + + return { + id: problem.id, + title: problem.title, + description: problem.description, + difficulty: problem.difficulty, + language: problem.language, + }; +}
{time}
{runInfo.category}
+ {run.category} +
+ + {run.views} +
- Watch runners compete in real time. Every second counts. + 42 live runs · Streaming in real time