Add problems pages
This commit is contained in:
parent
676b771122
commit
80fb1cdb3e
@ -35,3 +35,12 @@ model Submission {
|
|||||||
actualOutput String
|
actualOutput String
|
||||||
message String?
|
message String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Problem {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
friendlyName String
|
||||||
|
sampleInput String
|
||||||
|
sampleOutput String
|
||||||
|
realInput String
|
||||||
|
realOutput String
|
||||||
|
}
|
||||||
|
@ -24,7 +24,7 @@ export const handle = (async ({ event, resolve }) => {
|
|||||||
if (session) {
|
if (session) {
|
||||||
removeExpiredSessions(session.userId);
|
removeExpiredSessions(session.userId);
|
||||||
if (!isSessionExpired(session)) {
|
if (!isSessionExpired(session)) {
|
||||||
throw redirect(302, '/admin/reviews');
|
throw redirect(302, '/admin');
|
||||||
} else {
|
} else {
|
||||||
event.cookies.delete('token');
|
event.cookies.delete('token');
|
||||||
const res = resolve(event);
|
const res = resolve(event);
|
||||||
|
11
web/src/routes/admin/+layout.svelte
Normal file
11
web/src/routes/admin/+layout.svelte
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<header style="font-size: 24px" class="mt-2 d-flex align-items-center justify-content-center">
|
||||||
|
<ul class="nav col-12 col-md-auto justify-content-center">
|
||||||
|
<li><a href="/admin" class="nav-link px-2">Home</a></li>
|
||||||
|
<li><a href="/admin/reviews" class="nav-link px-2">Reviews</a></li>
|
||||||
|
<li><a href="/admin/problems" class="nav-link px-2">Problems</a></li>
|
||||||
|
<li><a href="/logout" class="nav-link px-2" data-sveltekit-preload-data="off">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</header>
|
||||||
|
<hr />
|
||||||
|
<slot />
|
||||||
|
<div style="height: 100px" />
|
5
web/src/routes/admin/+page.svelte
Normal file
5
web/src/routes/admin/+page.svelte
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<svelte:head>
|
||||||
|
<title>Admin Home</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<h1 style="text-align:center" class="mb-4">Admin Home</h1>
|
@ -56,7 +56,7 @@
|
|||||||
<title>Diff</title>
|
<title>Diff</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 class="mb-4">Diff</h1>
|
<h1 style="text-align:center" class="mb-4">Diff</h1>
|
||||||
|
|
||||||
<a href="/admin/reviews" class="btn btn-outline-primary">Back</a>
|
<a href="/admin/reviews" class="btn btn-outline-primary">Back</a>
|
||||||
<div class="mt-3" id="diff" />
|
<div class="mt-3" id="diff" />
|
||||||
|
11
web/src/routes/admin/problems/+page.server.ts
Normal file
11
web/src/routes/admin/problems/+page.server.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { db } from '$lib/server/prisma';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async () => {
|
||||||
|
const query = await db.problem.findMany();
|
||||||
|
return {
|
||||||
|
problems: query.map((row) => {
|
||||||
|
return { id: row.id, friendlyName: row.friendlyName };
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}) satisfies PageServerLoad;
|
29
web/src/routes/admin/problems/+page.svelte
Normal file
29
web/src/routes/admin/problems/+page.svelte
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Problems</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<h1 style="text-align:center" class="mb-4">Problems</h1>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="text-end">
|
||||||
|
<a href="/admin/problems/create" class="btn btn-outline-success">Create</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if data.problems.length === 0}
|
||||||
|
<div class="alert alert-warning">No problems</div>
|
||||||
|
{/if}
|
||||||
|
<div class="list-group">
|
||||||
|
{#each data.problems as problem}
|
||||||
|
<a
|
||||||
|
href={'/admin/problems/' + problem.id.toString()}
|
||||||
|
class="list-group-item list-group-item-action">{problem.friendlyName}</a
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</div>
|
46
web/src/routes/admin/problems/[problemId]/+page.server.ts
Normal file
46
web/src/routes/admin/problems/[problemId]/+page.server.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { db } from '$lib/server/prisma';
|
||||||
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ params }) => {
|
||||||
|
const problemId = parseInt(params.problemId);
|
||||||
|
if (isNaN(problemId)) {
|
||||||
|
throw error(400, 'Invalid request');
|
||||||
|
}
|
||||||
|
const query = await db.problem.findUnique({ where: { id: problemId } });
|
||||||
|
if (!query) {
|
||||||
|
throw redirect(302, '/admin/problems');
|
||||||
|
}
|
||||||
|
return { problemData: query };
|
||||||
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
edit: async ({ params, request }) => {
|
||||||
|
const problemId = parseInt(params.problemId);
|
||||||
|
if (isNaN(problemId)) {
|
||||||
|
throw error(400, 'Invalid problem');
|
||||||
|
}
|
||||||
|
const data = await request.formData();
|
||||||
|
const name = data.get('name');
|
||||||
|
const sampleInput = data.get('sampleInput');
|
||||||
|
const sampleOutput = data.get('sampleOutput');
|
||||||
|
const realInput = data.get('realInput');
|
||||||
|
const realOutput = data.get('realOutput');
|
||||||
|
if (!name || !sampleInput || !sampleOutput || !realInput || !realOutput) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.problem.update({
|
||||||
|
where: { id: problemId },
|
||||||
|
data: {
|
||||||
|
friendlyName: name.toString(),
|
||||||
|
sampleInput: sampleInput.toString(),
|
||||||
|
sampleOutput: sampleOutput.toString(),
|
||||||
|
realInput: realInput.toString(),
|
||||||
|
realOutput: realOutput.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
112
web/src/routes/admin/problems/[problemId]/+page.svelte
Normal file
112
web/src/routes/admin/problems/[problemId]/+page.svelte
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Actions, PageData } from './$types';
|
||||||
|
|
||||||
|
let editing = false;
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
export let form: Actions;
|
||||||
|
|
||||||
|
function stretchTextarea(textarea: HTMLTextAreaElement) {
|
||||||
|
textarea.style.height = textarea.scrollHeight + 'px';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 style="text-align:center" class="mb-4">{data.problemData.friendlyName}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<a href="/admin/problems" class="btn btn-outline-primary">Back</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
{#if !editing}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
if (!editing) {
|
||||||
|
editing = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
class="btn btn-warning">Edit</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if form && !form.success}
|
||||||
|
<div class="mt-3 alert alert-danger">Invalid edit</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form method="POST" action="?/edit">
|
||||||
|
<h4 style="text-align:center" class="mt-3">Name</h4>
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<textarea
|
||||||
|
name="name"
|
||||||
|
style="height:auto"
|
||||||
|
class="form-control"
|
||||||
|
disabled={!editing}
|
||||||
|
use:stretchTextarea>{data.problemData.friendlyName}</textarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h4 style="text-align:center" class="mt-5">Sample Data</h4>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<h5>Input</h5>
|
||||||
|
<textarea
|
||||||
|
name="sampleInput"
|
||||||
|
style="height:auto"
|
||||||
|
class="form-control"
|
||||||
|
disabled={!editing}
|
||||||
|
use:stretchTextarea>{data.problemData.sampleInput}</textarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<h5>Output</h5>
|
||||||
|
<textarea
|
||||||
|
name="sampleOutput"
|
||||||
|
style="height:auto"
|
||||||
|
class="form-control"
|
||||||
|
disabled={!editing}
|
||||||
|
use:stretchTextarea>{data.problemData.sampleOutput}</textarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 style="text-align:center" class="mt-5">Real Data</h4>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<h5>Input</h5>
|
||||||
|
<textarea
|
||||||
|
name="realInput"
|
||||||
|
style="height:auto"
|
||||||
|
class="form-control"
|
||||||
|
disabled={!editing}
|
||||||
|
use:stretchTextarea>{data.problemData.realInput}</textarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<h5>Output</h5>
|
||||||
|
<textarea
|
||||||
|
name="realOutput"
|
||||||
|
style="height:auto"
|
||||||
|
class="form-control"
|
||||||
|
disabled={!editing}
|
||||||
|
use:stretchTextarea>{data.problemData.realOutput}</textarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if editing}
|
||||||
|
<div class="mt-3 row">
|
||||||
|
<div class="text-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
on:click={async () => {
|
||||||
|
location.reload();
|
||||||
|
}}>Cancel</button
|
||||||
|
>
|
||||||
|
<button type="submit" class="btn btn-success">Update</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</form>
|
28
web/src/routes/admin/problems/create/+page.server.ts
Normal file
28
web/src/routes/admin/problems/create/+page.server.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { db } from '$lib/server/prisma';
|
||||||
|
import type { Actions } from './$types';
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
create: async ({ request }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const name = data.get('name');
|
||||||
|
const sampleInput = data.get('sampleInput');
|
||||||
|
const sampleOutput = data.get('sampleOutput');
|
||||||
|
const realInput = data.get('realInput');
|
||||||
|
const realOutput = data.get('realOutput');
|
||||||
|
if (!name || !sampleInput || !sampleOutput || !realInput || !realOutput) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.problem.create({
|
||||||
|
data: {
|
||||||
|
friendlyName: name.toString(),
|
||||||
|
sampleInput: sampleInput.toString(),
|
||||||
|
sampleOutput: sampleOutput.toString(),
|
||||||
|
realInput: realInput.toString(),
|
||||||
|
realOutput: realOutput.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
64
web/src/routes/admin/problems/create/+page.svelte
Normal file
64
web/src/routes/admin/problems/create/+page.svelte
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import type { Actions } from './$types';
|
||||||
|
|
||||||
|
export let form: Actions;
|
||||||
|
|
||||||
|
$: if (form && form.success) {
|
||||||
|
goto('/admin/problems');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Create Problem</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<h1 style="text-align:center" class="mb-4">Create Problem</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<a href="/admin/problems" class="btn btn-outline-primary">Back</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if form && !form.success}
|
||||||
|
<div class="mt-3 alert alert-danger">Invalid data</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form method="POST" action="?/create" use:enhance>
|
||||||
|
<h4 style="text-align:center" class="mt-3">Name</h4>
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<textarea name="name" class="form-control" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h4 style="text-align:center" class="mt-5">Sample Data</h4>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<h5>Input</h5>
|
||||||
|
<textarea name="sampleInput" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<h5>Output</h5>
|
||||||
|
<textarea name="sampleOutput" class="form-control" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 style="text-align:center" class="mt-5">Real Data</h4>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<h5>Input</h5>
|
||||||
|
<textarea name="realInput" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<h5>Output</h5>
|
||||||
|
<textarea name="realOutput" class="form-control" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 row">
|
||||||
|
<div class="text-end">
|
||||||
|
<button type="submit" class="btn btn-success">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -7,7 +7,11 @@ export const load = (async () => {
|
|||||||
query.sort((a, b) => {
|
query.sort((a, b) => {
|
||||||
return a.createdAt.valueOf() - b.createdAt.valueOf();
|
return a.createdAt.valueOf() - b.createdAt.valueOf();
|
||||||
});
|
});
|
||||||
return { reviewList: query };
|
return {
|
||||||
|
reviewList: query.map((row) => {
|
||||||
|
return { id: row.id, createdAt: row.createdAt };
|
||||||
|
})
|
||||||
|
};
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
@ -10,9 +10,7 @@
|
|||||||
<title>Reviews</title>
|
<title>Reviews</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 class="mb-4">Reviews</h1>
|
<h1 style="text-align:center" class="mb-4">Reviews</h1>
|
||||||
|
|
||||||
<a href="/logout" class="mb-2 btn btn-outline-primary" data-sveltekit-preload-data="off">Logout</a>
|
|
||||||
|
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{#if data.reviewList.length === 0}
|
{#if data.reviewList.length === 0}
|
||||||
|
Loading…
Reference in New Issue
Block a user