@@ -97,7 +97,13 @@ const ProblemCard = ({
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<CardFooter className="absolute bottom-4 right-4 p-0 opacity-0 group-hover:opacity-100 transition-all">
|
<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" />
|
<Zap className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import { getProblems } from "@/modules/problems/actions";
|
|||||||
export default function Problems() {
|
export default function Problems() {
|
||||||
const [problems, problemsLoaded, problemsError] = useAction(getProblems);
|
const [problems, problemsLoaded, problemsError] = useAction(getProblems);
|
||||||
|
|
||||||
console.log(problems);
|
|
||||||
|
|
||||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
||||||
|
|
||||||
const difficulties = ["All", "Easy", "Medium", "Hard"];
|
const difficulties = ["All", "Easy", "Medium", "Hard"];
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import { json } from "@codemirror/lang-json";
|
|||||||
import { python } from "@codemirror/lang-python";
|
import { python } from "@codemirror/lang-python";
|
||||||
import { java } from "@codemirror/lang-java";
|
import { java } from "@codemirror/lang-java";
|
||||||
import { useEffect, useState } from "react";
|
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 { getLiveRunMoves } from "@/modules/live-run/actions";
|
||||||
|
import { LiveRun, LiveRunMove } from "@/modules/live-run/types";
|
||||||
|
|
||||||
type Language =
|
type Language =
|
||||||
| "javascript"
|
| "javascript"
|
||||||
@@ -40,26 +41,19 @@ function getLanguageExtension(lang: Language) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Move = {
|
export default function Editor({ run }: { run: LiveRun | null | undefined }) {
|
||||||
latency: number;
|
|
||||||
cursor: number;
|
|
||||||
changes: {
|
|
||||||
from: number;
|
|
||||||
to: number;
|
|
||||||
insert: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Editor() {
|
|
||||||
const [editorView, setEditorView] = useState<EditorView>();
|
const [editorView, setEditorView] = useState<EditorView>();
|
||||||
const [moves] = useAction<Move[]>(getLiveRunMoves);
|
const [moves] = useActionOnce<LiveRunMove[]>(
|
||||||
|
async () => (run ? (await getLiveRunMoves(run.id)) || [] : []),
|
||||||
|
[run],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editorView || !moves) return;
|
if (!editorView || !moves) return;
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
const executeMove = async (move: Move) => {
|
const executeMove = async (move: LiveRunMove) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
if (!editorView || cancelled) return resolve();
|
if (!editorView || cancelled) return resolve();
|
||||||
@@ -1,28 +1,37 @@
|
|||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import formatTime from "@/lib/time-format";
|
import formatTime from "@/lib/time-format";
|
||||||
import { LiveRun } from "@/modules/live-run/types";
|
import { LiveRun } from "@/modules/live-run/types";
|
||||||
|
import { Eye } from "lucide-react";
|
||||||
import { useEffect, useState } from "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");
|
const [time, setTime] = useState("00:00:00");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Update function calculates elapsed time
|
// 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
|
// Update immediately and then every second
|
||||||
updateTime();
|
updateTime();
|
||||||
const interval = setInterval(updateTime, 50);
|
const interval = setInterval(updateTime, 50);
|
||||||
|
|
||||||
return () => clearInterval(interval); // cleanup on unmount
|
return () => clearInterval(interval); // cleanup on unmount
|
||||||
}, [runInfo.start]);
|
}, [run.start]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="relative text-center overflow-hidden h-full">
|
<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" />
|
<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">
|
<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="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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -6,13 +6,24 @@ import RunChat, { ChatMessage } from "./components/RunChat";
|
|||||||
import { FileTree } from "./components/FileTree";
|
import { FileTree } from "./components/FileTree";
|
||||||
import { File } from "lucide-react";
|
import { File } from "lucide-react";
|
||||||
import Editor from "./components/Editor";
|
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 { 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 ProfileCard from "./components/ProfileCard";
|
||||||
|
import { getProblem } from "@/modules/problems/actions";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
|
||||||
export default function Run() {
|
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[] = [
|
const chatMessages: ChatMessage[] = [
|
||||||
{ user: "Alice", message: "Nice split!", time: "1:53" },
|
{ user: "Alice", message: "Nice split!", time: "1:53" },
|
||||||
@@ -40,21 +51,11 @@ export default function Run() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 animate-fade-in opacity-0 stagger-2">
|
<div className="flex-1 animate-fade-in opacity-0 stagger-2">
|
||||||
{runInfo && <RunTimer runInfo={runInfo} />}
|
{run && <RunTimer run={run} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-2">
|
<div className="flex-2">
|
||||||
<ProblemCard
|
{problem && <ProblemCard index={4} problem={problem} />}
|
||||||
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",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ export default function Run() {
|
|||||||
<File size={16} /> myfile.js
|
<File size={16} /> myfile.js
|
||||||
</header>
|
</header>
|
||||||
<div className="flex-1 pt-3 pb-4 md:pb-5 w-full h-full">
|
<div className="flex-1 pt-3 pb-4 md:pb-5 w-full h-full">
|
||||||
<Editor />
|
<Editor run={run} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Eye } from "lucide-react";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { TableRow, TableCell } from "@/components/ui/table";
|
import { TableRow, TableCell } from "@/components/ui/table";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { LiveRun } from "@/modules/live-run/types";
|
import { LiveRun } from "@/modules/live-run/types";
|
||||||
import formatTime from "@/lib/time-format";
|
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({
|
export default function LiveRunRow({
|
||||||
index,
|
index,
|
||||||
@@ -15,8 +18,10 @@ export default function LiveRunRow({
|
|||||||
index: number;
|
index: number;
|
||||||
run: LiveRun;
|
run: LiveRun;
|
||||||
}) {
|
}) {
|
||||||
|
const [problem] = useAction(() => getProblem(run.problem));
|
||||||
|
|
||||||
const now = new Date().getTime() + 1000;
|
const now = new Date().getTime() + 1000;
|
||||||
const isTop = false;
|
const isTop = run.views > 5;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
@@ -29,57 +34,45 @@ export default function LiveRunRow({
|
|||||||
animationFillMode: "forwards",
|
animationFillMode: "forwards",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TableCell className="font-medium pl-5">
|
{/* Runner */}
|
||||||
|
<TableCell className="font-medium pl-5 py-5">
|
||||||
{isTop ? `🔥 ${run.username}` : run.username}
|
{isTop ? `🔥 ${run.username}` : run.username}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
{/* Problem + Category */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-col gap-1">
|
<Link
|
||||||
<span>{run.problem}</span>
|
href={`problems/${run.problem}`}
|
||||||
<Badge variant="outline" className="w-fit text-xs">
|
className="hover:text-primary flex items-center gap-1"
|
||||||
{run.category}
|
>
|
||||||
</Badge>
|
<LinkIcon className="w-3 h-3" />
|
||||||
</div>
|
{problem?.title || run.problem}
|
||||||
|
</Link>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
{/* Runtime */}
|
||||||
<TableCell
|
<TableCell
|
||||||
className={`font-mono ${isTop ? "text-primary font-bold" : ""}`}
|
className={`font-mono ${isTop ? "text-primary font-bold" : ""}`}
|
||||||
>
|
>
|
||||||
{formatTime(now - run.start, false)}
|
{formatTime(now - run.start, false)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
{/* Views */}
|
||||||
<Badge
|
<TableCell className="text-center">
|
||||||
className={
|
{isTop ? <Badge>{run.views}</Badge> : run.views}
|
||||||
false
|
|
||||||
? "bg-red-500/10 text-red-400"
|
|
||||||
: "bg-green-500/10 text-green-400"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
0
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
{/* Category */}
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<TableCell className="text-center">{run.category}</TableCell>
|
||||||
<span
|
|
||||||
className={`h-2 w-2 rounded-full ${
|
|
||||||
status === "pb"
|
|
||||||
? "bg-green-500"
|
|
||||||
: status === "danger"
|
|
||||||
? "bg-red-500"
|
|
||||||
: "bg-emerald-400"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
Live
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="text-right pr-5">
|
{/* Actions */}
|
||||||
|
<TableCell className="pr-5 text-end">
|
||||||
|
<Link href={`/runs/${run.id}`}>
|
||||||
<Button size="sm" variant={isTop ? "default" : "ghost"}>
|
<Button size="sm" variant={isTop ? "default" : "ghost"}>
|
||||||
<Eye className="h-4 w-4 mr-1" />
|
<Eye className="h-4 w-4 mr-1" />
|
||||||
Watch
|
Watch
|
||||||
</Button>
|
</Button>
|
||||||
|
</Link>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|||||||
+10
-30
@@ -1,27 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Zap, Users, Timer, TrendingUp, Eye } from "lucide-react";
|
import { Card } from "@/components/ui/card";
|
||||||
|
|
||||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableCell,
|
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import LiveRunRow from "./components/LiveRunRow";
|
import LiveRunRow from "./components/LiveRunRow";
|
||||||
import StatCard from "@/components/app/StatCard";
|
|
||||||
import useAction from "@/hook/use-action";
|
import useAction from "@/hook/use-action";
|
||||||
import { getLiveRuns } from "@/modules/live-run/actions";
|
import { getLiveRuns } from "@/modules/live-run/actions";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
|
|
||||||
export default function LiveRuns() {
|
export default function LiveRuns() {
|
||||||
const [liveRuns, liveRunsLoaded, liveRunsError] = useAction(getLiveRuns);
|
const [liveRuns] = useAction(getLiveRuns);
|
||||||
|
|
||||||
// Sort top runners (status === 'pb') to the top
|
// Sort top runners (status === 'pb') to the top
|
||||||
const sortedRuns = [...(liveRuns || [])];
|
const sortedRuns = [...(liveRuns || [])];
|
||||||
@@ -33,30 +26,17 @@ export default function LiveRuns() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-bold">Live Runs</h1>
|
<h1 className="text-4xl font-bold">Live Runs</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Watch runners compete in real time. Every second counts.
|
42 live runs · Streaming in real time
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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 */}
|
{/* Filters */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<ToggleGroup variant="outline" type="single" defaultValue="any">
|
<ToggleGroup variant="outline" type="single">
|
||||||
<ToggleGroupItem value="any" aria-label="any%">
|
<ToggleGroupItem value="any%" aria-label="any%">
|
||||||
any%
|
any%
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="100" aria-label="100%">
|
<ToggleGroupItem value="100%" aria-label="100%">
|
||||||
100%
|
100%
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
@@ -69,10 +49,10 @@ export default function LiveRuns() {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="pl-5">Runner</TableHead>
|
<TableHead className="pl-5">Runner</TableHead>
|
||||||
<TableHead>Problem</TableHead>
|
<TableHead>Problem</TableHead>
|
||||||
<TableHead>Time</TableHead>
|
<TableHead className="w-20 text-center">Time</TableHead>
|
||||||
<TableHead>Pace</TableHead>
|
<TableHead className="w-20 text-center">Views</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead className="w-20 text-center">Category</TableHead>
|
||||||
<TableHead className="pr-5" />
|
<TableHead className="pr-5 w-24" />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
|
|
||||||
|
|||||||
+47
-1
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
|
|
||||||
export default function useAction<T>(
|
export default function useAction<T>(
|
||||||
action: () => Promise<T>,
|
action: () => Promise<T>,
|
||||||
dependencies = [],
|
dependencies: any[] = [],
|
||||||
): [T | null, boolean, Error | null] {
|
): [T | null, boolean, Error | null] {
|
||||||
const [value, setValue] = useState<T | null>(null);
|
const [value, setValue] = useState<T | null>(null);
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
@@ -17,3 +17,49 @@ export default function useAction<T>(
|
|||||||
|
|
||||||
return [value, loaded, error];
|
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];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,94 +1,16 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { createClient } from "@/lib/supabase/server";
|
|
||||||
import { getSession, getSessionRedirect } from "../account/core";
|
|
||||||
import * as Core from "./core";
|
import * as Core from "./core";
|
||||||
import { LiveRun } from "./types";
|
import { LiveRun } from "./types";
|
||||||
|
|
||||||
export async function getLiveRuns(): Promise<LiveRun[]> {
|
export async function getLiveRuns(): Promise<LiveRun[]> {
|
||||||
const supabase = await createClient();
|
return Core.getLiveRuns();
|
||||||
|
|
||||||
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(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLiveRun(): Promise<LiveRun> {
|
export async function getLiveRun(id: string): Promise<LiveRun | undefined> {
|
||||||
return {
|
return Core.getLiveRun(id);
|
||||||
id: "0",
|
|
||||||
username: "selimaj",
|
|
||||||
start: new Date().getTime(),
|
|
||||||
problem: "solve",
|
|
||||||
category: "any%",
|
|
||||||
runsCount: 0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLiveRunMoves() {
|
export async function getLiveRunMoves(id: string) {
|
||||||
const code = `// Import the necessary React library features
|
return Core.getLiveRunMoves(id);
|
||||||
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,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,40 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import "server-only";
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
export interface LiveRun {
|
export interface LiveRun {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
views: number;
|
||||||
start: number;
|
start: number;
|
||||||
runsCount: number;
|
runsCount: number;
|
||||||
problem: string;
|
problem: string;
|
||||||
category: "any%" | "100%";
|
category: "any%" | "100%";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LiveRunMove {
|
||||||
|
latency: number;
|
||||||
|
cursor: number;
|
||||||
|
changes?: {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
insert: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,3 +6,7 @@ import { Problem } from "./types";
|
|||||||
export async function getProblems(): Promise<Problem[]> {
|
export async function getProblems(): Promise<Problem[]> {
|
||||||
return await Core.getProblems();
|
return await Core.getProblems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getProblem(id: string): Promise<Problem | undefined> {
|
||||||
|
return await Core.getProblem(id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import "server-only";
|
import "server-only";
|
||||||
import { ProblemDefinition } from "./types";
|
import { Problem, ProblemDefinition } from "./types";
|
||||||
|
|
||||||
const problems: ProblemDefinition[] = [
|
const problems: ProblemDefinition[] = [
|
||||||
{
|
{
|
||||||
@@ -20,7 +20,7 @@ const problems: ProblemDefinition[] = [
|
|||||||
description:
|
description:
|
||||||
"Production build fails while development works. Fix the build.",
|
"Production build fails while development works. Fix the build.",
|
||||||
language: "javascript",
|
language: "javascript",
|
||||||
difficulty: "easy",
|
difficulty: "hard",
|
||||||
validate100: () => true,
|
validate100: () => true,
|
||||||
validateAny: () => true,
|
validateAny: () => true,
|
||||||
},
|
},
|
||||||
@@ -29,13 +29,13 @@ const problems: ProblemDefinition[] = [
|
|||||||
title: "Implement Missing API Endpoint",
|
title: "Implement Missing API Endpoint",
|
||||||
description: "The frontend expects `/api/me`. Implement it correctly.",
|
description: "The frontend expects `/api/me`. Implement it correctly.",
|
||||||
language: "go",
|
language: "go",
|
||||||
difficulty: "easy",
|
difficulty: "medium",
|
||||||
validate100: () => true,
|
validate100: () => true,
|
||||||
validateAny: () => true,
|
validateAny: () => true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function getProblems() {
|
export async function getProblems(): Promise<Problem[]> {
|
||||||
return problems.map((def) => ({
|
return problems.map((def) => ({
|
||||||
id: def.id,
|
id: def.id,
|
||||||
title: def.title,
|
title: def.title,
|
||||||
@@ -44,3 +44,17 @@ export async function getProblems() {
|
|||||||
language: def.language,
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user