Merge pull request #12 from orus-dev/ui-api-prep

UI api prep
This commit is contained in:
Klesti Selimaj
2026-02-08 02:44:34 +01:00
committed by GitHub
17 changed files with 209 additions and 194 deletions
+7 -1
View File
@@ -97,7 +97,13 @@ const ProblemCard = ({
{/* Footer */}
<CardFooter className="absolute bottom-4 right-4 p-0 opacity-0 group-hover:opacity-100 transition-all">
<Button size="icon">
<Button
size="icon"
onClick={async () => {
await fetch(`http://127.0.0.1:63780/start-run/${problem.id}`);
window.open("vscode://", "_blank");
}}
>
<Zap className="h-4 w-4" />
</Button>
</CardFooter>
-2
View File
@@ -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"];
@@ -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<EditorView>();
const [moves] = useAction<Move[]>(getLiveRunMoves);
const [moves] = useActionOnce<LiveRunMove[]>(
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<void>((resolve) => {
const timeout = setTimeout(() => {
if (!editorView || cancelled) return resolve();
@@ -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 (
<Card className="relative text-center overflow-hidden h-full">
<div className="absolute inset-0 bg-linear-to-b from-primary/5 to-transparent pointer-events-none" />
<CardContent className="relative flex flex-col justify-center items-center h-full">
<p className="timer-display text-primary text-3xl font-bold">{time}</p>
<p className="text-muted-foreground">{runInfo.category}</p>
<span className="flex gap-4">
<p className="text-muted-foreground flex gap-1 items-center">
{run.category}
</p>
<p className="text-muted-foreground flex gap-1 items-center">
<Eye className="w-4 h-4" />
{run.views}
</p>
</span>
</CardContent>
</Card>
);
@@ -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() {
</div>
<div className="flex-1 animate-fade-in opacity-0 stagger-2">
{runInfo && <RunTimer runInfo={runInfo} />}
{run && <RunTimer run={run} />}
</div>
<div className="flex-2">
<ProblemCard
index={4}
problem={{
id: "fix-auth-callback",
title: "Fix the Broken Auth Callback",
description:
"Users can log in with OAuth, but sessions aren't persisted. Fix it.",
language: "typescript",
difficulty: "easy",
}}
/>
{problem && <ProblemCard index={4} problem={problem} />}
</div>
</div>
@@ -85,7 +86,7 @@ export default function Run() {
<File size={16} /> myfile.js
</header>
<div className="flex-1 pt-3 pb-4 md:pb-5 w-full h-full">
<Editor />
<Editor run={run} />
</div>
</div>
</CardContent>
+32 -39
View File
@@ -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 (
<TableRow
@@ -29,57 +34,45 @@ export default function LiveRunRow({
animationFillMode: "forwards",
}}
>
<TableCell className="font-medium pl-5">
{/* Runner */}
<TableCell className="font-medium pl-5 py-5">
{isTop ? `🔥 ${run.username}` : run.username}
</TableCell>
{/* Problem + Category */}
<TableCell>
<div className="flex flex-col gap-1">
<span>{run.problem}</span>
<Badge variant="outline" className="w-fit text-xs">
{run.category}
</Badge>
</div>
<Link
href={`problems/${run.problem}`}
className="hover:text-primary flex items-center gap-1"
>
<LinkIcon className="w-3 h-3" />
{problem?.title || run.problem}
</Link>
</TableCell>
{/* Runtime */}
<TableCell
className={`font-mono ${isTop ? "text-primary font-bold" : ""}`}
>
{formatTime(now - run.start, false)}
</TableCell>
<TableCell>
<Badge
className={
false
? "bg-red-500/10 text-red-400"
: "bg-green-500/10 text-green-400"
}
>
0
</Badge>
{/* Views */}
<TableCell className="text-center">
{isTop ? <Badge>{run.views}</Badge> : run.views}
</TableCell>
<TableCell>
<div className="flex items-center gap-2 text-sm">
<span
className={`h-2 w-2 rounded-full ${
status === "pb"
? "bg-green-500"
: status === "danger"
? "bg-red-500"
: "bg-emerald-400"
}`}
/>
Live
</div>
</TableCell>
{/* Category */}
<TableCell className="text-center">{run.category}</TableCell>
<TableCell className="text-right pr-5">
<Button size="sm" variant={isTop ? "default" : "ghost"}>
<Eye className="h-4 w-4 mr-1" />
Watch
</Button>
{/* Actions */}
<TableCell className="pr-5 text-end">
<Link href={`/runs/${run.id}`}>
<Button size="sm" variant={isTop ? "default" : "ghost"}>
<Eye className="h-4 w-4 mr-1" />
Watch
</Button>
</Link>
</TableCell>
</TableRow>
);
+10 -30
View File
@@ -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() {
<div>
<h1 className="text-4xl font-bold">Live Runs</h1>
<p className="text-muted-foreground">
Watch runners compete in real time. Every second counts.
42 live runs · Streaming in real time
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<StatCard icon={Zap} label="Live Runs" value="42" index={0} />
<StatCard icon={Users} label="Active Runners" value="39" index={1} />
<StatCard icon={Timer} label="Avg Runtime" value="1:47" index={2} />
<StatCard
icon={TrendingUp}
label="PB Pace Runs"
value="7"
index={3}
/>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-2">
<ToggleGroup variant="outline" type="single" defaultValue="any">
<ToggleGroupItem value="any" aria-label="any%">
<ToggleGroup variant="outline" type="single">
<ToggleGroupItem value="any%" aria-label="any%">
any%
</ToggleGroupItem>
<ToggleGroupItem value="100" aria-label="100%">
<ToggleGroupItem value="100%" aria-label="100%">
100%
</ToggleGroupItem>
</ToggleGroup>
@@ -69,10 +49,10 @@ export default function LiveRuns() {
<TableRow>
<TableHead className="pl-5">Runner</TableHead>
<TableHead>Problem</TableHead>
<TableHead>Time</TableHead>
<TableHead>Pace</TableHead>
<TableHead>Status</TableHead>
<TableHead className="pr-5" />
<TableHead className="w-20 text-center">Time</TableHead>
<TableHead className="w-20 text-center">Views</TableHead>
<TableHead className="w-20 text-center">Category</TableHead>
<TableHead className="pr-5 w-24" />
</TableRow>
</TableHeader>
+47 -1
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
export default function useAction<T>(
action: () => Promise<T>,
dependencies = [],
dependencies: any[] = [],
): [T | null, boolean, Error | null] {
const [value, setValue] = useState<T | null>(null);
const [loaded, setLoaded] = useState(false);
@@ -17,3 +17,49 @@ export default function useAction<T>(
return [value, loaded, error];
}
export function useActionOnce<T>(
action: () => Promise<T>,
dependencies: any[] = [],
): [T | null, boolean, Error | null] {
const [value, setValue] = useState<T | null>(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<T>(
action: () => Promise<T>,
interval: number,
dependencies: any[] = [],
): [T | null, boolean, Error | null] {
const [value, setValue] = useState<T | null>(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];
}
+5 -83
View File
@@ -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<LiveRun[]> {
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<LiveRun> {
return {
id: "0",
username: "selimaj",
start: new Date().getTime(),
problem: "solve",
category: "any%",
runsCount: 0,
};
export async function getLiveRun(id: string): Promise<LiveRun | undefined> {
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 (
<h1>Hello, world!</h1>
);
}
// Target a DOM element in your HTML (e.g., <div id="root"></div>)
const domNode = document.getElementById('root');
// Create a root and render your component into the DOM
const root = createRoot(domNode);
root.render(<App />);`.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);
}
+37
View File
@@ -1,3 +1,40 @@
"use server";
import "server-only";
import { LiveRun, LiveRunMove } from "./types";
const liveRuns: LiveRun[] = [];
const liveRunMoves: Record<string, LiveRunMove[]> = {};
export async function getLiveRuns(): Promise<LiveRun[]> {
return liveRuns;
}
export async function getLiveRun(id: string): Promise<LiveRun | undefined> {
return liveRuns.find((r) => r.id === id);
}
export async function getLiveRunMoves(
id: string,
): Promise<LiveRunMove[] | undefined> {
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);
}
+11
View File
@@ -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;
};
}
+4
View File
@@ -6,3 +6,7 @@ import { Problem } from "./types";
export async function getProblems(): Promise<Problem[]> {
return await Core.getProblems();
}
export async function getProblem(id: string): Promise<Problem | undefined> {
return await Core.getProblem(id);
}
+18 -4
View File
@@ -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<Problem[]> {
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<Problem | undefined> {
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,
};
}