Skip to content

Commit a243a5c

Browse files
committed
feat: Add backend validation!
1 parent 6c3ad59 commit a243a5c

File tree

7 files changed

+126
-31
lines changed

7 files changed

+126
-31
lines changed

bun.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"react-dom": "^19",
2121
"react-error-boundary": "^6.0.0",
2222
"react-hook-form": "^7.54.2",
23+
"react-spinners": "^0.17.0",
2324
"tailwind-merge": "^3.0.1",
2425
"tailwindcss": "^4.0.6",
2526
"tailwindcss-animate": "^1.0.7",
@@ -177,6 +178,8 @@
177178

178179
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
179180

181+
"react-spinners": ["react-spinners@0.17.0", "", { "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-L/8HTylaBmIWwQzIjMq+0vyaRXuoAevzWoD35wKpNTxxtYXWZp+xtgkfD7Y4WItuX0YvdxMPU79+7VhhmbmuTQ=="],
182+
180183
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
181184

182185
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"react-dom": "^19",
2828
"react-error-boundary": "^6.0.0",
2929
"react-hook-form": "^7.54.2",
30+
"react-spinners": "^0.17.0",
3031
"tailwind-merge": "^3.0.1",
3132
"tailwindcss": "^4.0.6",
3233
"tailwindcss-animate": "^1.0.7",

src/checker/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import path from 'path';
2+
import { tsCheck } from './tsCheck';
3+
4+
export async function validate(input: string, template: string): Promise<boolean> {
5+
const templateData = await Bun.file(path.join(__dirname, '..', '..', 'templates', `${template}.template.ts`)).text();
6+
const parts = templateData.split(/\/\* (?:SHORT|LONG)_INPUT \*\//);
7+
if (!input.startsWith(parts[0]) || !input.endsWith(parts.at(-1))) return false;
8+
9+
let remainingText = input;
10+
while (parts.length > 0) {
11+
const nextIndex = remainingText.indexOf(parts.shift());
12+
if (nextIndex === -1) return false;
13+
remainingText = remainingText.slice(nextIndex);
14+
}
15+
return tsCheck(input);
16+
}
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ import path from 'path';
33

44
const inputsDir = path.join(__dirname, '..', '..', 'inputs');
55

6-
async function check(input: string): Promise<boolean> {
6+
export async function tsCheck(input: string): Promise<boolean> {
77
if (!input) throw new Error('Input not found.');
88
const id = Bun.randomUUIDv7();
99
const fileName = `${id}.template.ts`;
1010
await Bun.write(path.join(inputsDir, fileName), input);
1111
let success: boolean;
1212
try {
13-
await $`bunx tsc --noEmit --strict --strictNullChecks ${fileName}`.cwd(inputsDir);
13+
await $`bunx tsc --noEmit --strict --strictNullChecks --skipLibCheck ${fileName}`
14+
.cwd(inputsDir)
15+
.env({ NODE_OPTIONS: '--max-old-space-size=100' })
16+
.quiet();
1417
success = true;
1518
} catch {
1619
success = false;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { type ReactElement, useState } from 'react';
2+
import { Input } from '@/components/ui/input';
3+
4+
const NAME_KEY = '%%%NAME%%%';
5+
6+
export function getName(): string | undefined {
7+
return localStorage.getItem(NAME_KEY);
8+
}
9+
10+
export function NameInput({}): ReactElement {
11+
const [value, setValue] = useState(() => localStorage.getItem(NAME_KEY) || '');
12+
return (
13+
<Input
14+
value={value}
15+
onChange={e => {
16+
localStorage.setItem(NAME_KEY, e.target.value);
17+
setValue(e.target.value);
18+
}}
19+
className="inline-flex max-w-54"
20+
required
21+
placeholder="Your name?"
22+
/>
23+
);
24+
}

src/components/molecules/TemplateRenderer.tsx

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,16 @@
1-
import {
2-
type ReactElement,
3-
type ReactNode,
4-
Suspense,
5-
use,
6-
useCallback,
7-
useEffect,
8-
useMemo,
9-
useRef,
10-
useState,
11-
} from 'react';
1+
import { type ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react';
122
import { groupSub } from 'group-sub';
13-
import { ErrorBoundary } from 'react-error-boundary';
143

154
import { ShortInput } from '@/components/ui/ShortInput';
165
import { LongInput } from '@/components/ui/LongInput';
176
import { ContentCard } from '@/components/atoms/ContentCard';
187
import { Button } from '@/components/ui/button';
198
import { StaticEditor } from '@/components/molecules/StaticEditor';
209
import { useFetch } from '@/hooks/useFetch';
10+
import { getName, NameInput } from '@/components/molecules/NameInput';
11+
import { ClimbingBoxLoader, PacmanLoader } from 'react-spinners';
2112

22-
function BaseTemplateRenderer({
23-
templateCode,
24-
onSubmit,
25-
}: {
26-
templateCode: string;
27-
onSubmit: (code: string) => void;
28-
}): ReactElement {
13+
function BaseTemplateRenderer({ templateCode, template }: { templateCode: string; template: string }): ReactElement {
2914
const [currentCode, setCurrentCode] = useState(() => templateCode);
3015

3116
const onChange = useRef<() => void | null>(null);
@@ -66,14 +51,63 @@ function BaseTemplateRenderer({
6651
};
6752
}, [getCurrentCode]);
6853

54+
const [submitting, setSubmitting] = useState(false);
55+
const [status, setStatus] = useState<boolean | null>(null);
56+
57+
const handleSubmit = useCallback<() => void>(() => {
58+
const code = getCurrentCode();
59+
const name = getName();
60+
61+
if (!code || !name) return;
62+
63+
setSubmitting(true);
64+
fetch(`/api/submit/${template}`, {
65+
method: 'POST',
66+
headers: { 'Content-Type': 'application/json' },
67+
body: JSON.stringify({ name, code }),
68+
}).then(res => {
69+
if (!res.ok) {
70+
setStatus(false);
71+
setSubmitting(false);
72+
} else {
73+
setStatus(true);
74+
setSubmitting(false);
75+
// TODO: Store in localStorage
76+
}
77+
});
78+
}, [getCurrentCode, template]);
79+
6980
return (
7081
<div className="flex gap-24 w-full justify-around m-12 max-w-full">
7182
<ContentCard className="grow shrink basis-0 min-w-0">
7283
<pre className="text-start">
7384
{parts.map(part => (part === '\n' ? <br /> : typeof part === 'string' ? part : part.component))}
7485
</pre>
7586
<hr className="m-6 border-zinc-300 dark:border-zinc-600" />
76-
<Button onClick={() => console.log(getCurrentCode())}>Submit!</Button>
87+
<form className="flex gap-4 justify-center" action={handleSubmit}>
88+
<NameInput />
89+
<Button disabled={submitting} className="w-32">
90+
{submitting ? 'Submitting...' : 'Submit!'}
91+
</Button>
92+
{/*TODO*/}
93+
<div className="w-3 h-3">
94+
{submitting ? (
95+
<PacmanLoader
96+
size={12}
97+
color="var(--foreground)"
98+
className="mt-1"
99+
speedMultiplier={1.5}
100+
cssOverride={{ width: 0 }}
101+
/>
102+
) : typeof status === 'boolean' ? (
103+
status ? (
104+
':D'
105+
) : (
106+
':<'
107+
)
108+
) : null}
109+
</div>
110+
</form>
77111
</ContentCard>
78112
<ContentCard className="grow shrink basis-0 min-w-0">
79113
<StaticEditor code={currentCode} />
@@ -82,19 +116,13 @@ function BaseTemplateRenderer({
82116
);
83117
}
84118

85-
export function TemplateRenderer({
86-
template,
87-
onSubmit,
88-
}: {
89-
template: string;
90-
onSubmit: (code: string) => void;
91-
}): ReactElement {
119+
export function TemplateRenderer({ template }: { template: string; onSubmit: (code: string) => void }): ReactElement {
92120
const { data, loading, error } = useFetch<{ template: string }>(`/api/templates/${template}`);
93121

94122
return (
95123
<>
96124
{loading ? <ContentCard>Loading...</ContentCard> : null}
97-
{data ? <BaseTemplateRenderer onSubmit={onSubmit} templateCode={data.template} /> : null}
125+
{data ? <BaseTemplateRenderer templateCode={data.template} template={template} /> : null}
98126
{error ? <ContentCard>Template not found.</ContentCard> : null}
99127
</>
100128
);

src/index.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { serve } from 'bun';
2+
import z from 'zod';
23
import path from 'path';
34
import index from '@/index.html';
45
import { readdir } from 'fs/promises';
6+
import { validate } from '@/checker';
57

68
const server = serve({
79
routes: {
@@ -19,7 +21,7 @@ const server = serve({
1921
try {
2022
const templateData = await Bun.file(path.join(__dirname, '..', 'templates', `${template}.template.ts`)).text();
2123
return Response.json({ template: templateData });
22-
} catch (e) {
24+
} catch {
2325
return Response.json({ message: 'Not found' }, { status: 404 });
2426
}
2527
},
@@ -32,6 +34,24 @@ const server = serve({
3234
return Response.json({ exercises });
3335
},
3436

37+
'/api/submit/:exercise': {
38+
POST: async req => {
39+
const exercise = req.params.exercise;
40+
const reqData = z.object({ code: z.string(), name: z.string() }).safeParse(await req.json());
41+
if (!reqData.success) return Response.json({ message: 'Invalid input' }, { status: 400 });
42+
43+
try {
44+
const success = await validate(reqData.data.code, exercise);
45+
if (success) {
46+
// TODO: Dispatch to socket and store in state
47+
return Response.json({ message: 'Valid input!' });
48+
} else return Response.json({ message: 'Failed.' }, { status: 400 });
49+
} catch {
50+
return Response.json({ message: 'Not found' }, { status: 404 });
51+
}
52+
},
53+
},
54+
3555
'/favicon.ico': new Response(await Bun.file(path.join(__dirname, '..', 'assets', 'logo.png')).bytes(), {
3656
headers: { 'Content-Type': 'image/x-icon' },
3757
}),

0 commit comments

Comments
 (0)