Commit 3a96f983 authored by Yechang's avatar Yechang
Browse files

feat(submission): support language and filename requirement

parent e8ee1811
Loading
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
-- AlterTable
ALTER TABLE `Problem` ADD COLUMN `accept` JSON NOT NULL;
+2 −0
Original line number Diff line number Diff line
@@ -190,6 +190,8 @@ model Problem {

  gradeGroup   GradeGroup? @relation(fields: [gradeGroupId], references: [id])
  gradeGroupId Int?        @unique

  accept Json @default("{\"type\": \"code\",\"languages\": [\"cpp\", \"java\"]}")
}

model Submission {
+1 −1
Original line number Diff line number Diff line
@@ -9,7 +9,7 @@
	let editorContainer: HTMLElement;

	export let value: string;
	export let language: 'cpp' | 'java' | 'json';
	export let language: 'cpp' | 'java' | 'json' | 'python';
	export let readOnly: boolean = false;

	let mounted = false;
+15 −175
Original line number Diff line number Diff line
<script lang="ts" context="module">
	import { z } from 'zod';
	export const submissionFormSchema = z.object({
		code: z.string(),
		language: z.enum(['cpp', 'java']),
		key: z.string().optional()
	});
	export type SubmissionFormSchema = typeof submissionFormSchema;

	export const codeFiles = [
		{
			language: 'cpp',
			filename: 'main.cpp'
		},
		{
			language: 'java',
			filename: 'Main.java'
		}
	] as const;
	export const dict = _.keyBy(codeFiles, 'language');
	import _ from 'lodash';
</script>

<script lang="ts">
	import * as Form from '$lib/components/ui/form';
	import { buttonVariants } from '$lib/components/ui/button';
	import * as Dialog from '$lib/components/ui/dialog';
	import * as Card from '$lib/components/ui/card';
	import * as Avatar from '$lib/components/ui/avatar';
	import { cn } from '$lib/utils';
	import { EyeNone, FileText, Upload } from 'svelte-radix';
	import MonacoEditor from '../monaco/MonacoEditor.svelte';
	import * as Select from '$lib/components/ui/select';
	let className: string | undefined | null = undefined;
	export { className as class };
	import { type SuperValidated, type Infer, superForm, type FormPath } from 'sveltekit-superforms';
	import SuperDebug from 'sveltekit-superforms';
	import { zodClient } from 'sveltekit-superforms/adapters';
	import { browser, dev } from '$app/environment';
	import _ from 'lodash';
	import { upload } from '$lib/shared/file';
	import { goto } from '$app/navigation';
	import Separator from '$lib/components/ui/separator/separator.svelte';
	import moment from 'moment';
	import { acceptSchema } from '$lib/shared/submission';
	import CodeSubmitCard, { type SubmissionFormSchema } from './SubmitCard/CodeSubmitCard.svelte';
	import type { Infer, SuperValidated } from 'sveltekit-superforms';

	export let data: SuperValidated<Infer<SubmissionFormSchema>>;
	const form = superForm(data, {
		dataType: 'json',
		async onSubmit({ jsonData }) {
			const blob = new Blob([$formData.code], { type: 'plain/text' });
			const file = new File([blob], dict[$formData.language].filename);
			const key = await upload(file);
			const data = Object.fromEntries(_.concat(_.entries($formData), [['key', key]]));
			jsonData(data);
		},
		validators: zodClient(submissionFormSchema),
		delayMs: 500,
		timeoutMs: 8000,
		applyAction: false,
		onResult({ result }) {
			if (result.type === 'redirect') {
				goto(result.location);
			}
		}
	});

	export let accept: z.infer<typeof acceptSchema>;
	export let submissions: Array<{ time: Date; result: string; id: number }> = [];
	export let form: SuperValidated<Infer<SubmissionFormSchema>>;

	const { form: formData, enhance, delayed, timeout } = form;
	$: selectedLanguage = {
		label: dict[$formData.language].filename,
		value: $formData.language
	};
	let className: string | undefined | null = undefined;
	export { className as class };
</script>

<Card.Root class={cn(className)} {...$$restProps}>
@@ -78,122 +27,13 @@
		<!-- <Card.Description>Choose what you want to be notified about.</Card.Description> -->
	</Card.Header>
	<Card.Content class="grid gap-1">
		<Dialog.Root>
			<Dialog.Trigger class={buttonVariants({ variant: 'outline' })}>
				<Upload class="mr-2 h-4 w-4" />
				Create
			</Dialog.Trigger>
			<Dialog.Content class="h-[60%] sm:max-h-[80%] sm:max-w-[60%]">
				<form method="POST" class="flex flex-col" use:enhance>
					<Dialog.Header class="flex-none">
						<Dialog.Title>Create submission</Dialog.Title>
						<!-- <Dialog.Description>
						Make changes to your profile here. Click save when you're done.
					</Dialog.Description> -->
					</Dialog.Header>
					<div class="my-1">
						<Form.Field {form} name="language">
							<Form.Control let:attrs>
								<div class="flex flex-row items-center space-x-1">
									<Form.Label>Filename</Form.Label>
									<Select.Root
										selected={selectedLanguage}
										onSelectedChange={(s) => {
											s && ($formData.language = s.value);
										}}
									>
										<Select.Trigger class="w-[180px]">
											<Select.Value placeholder="File name" />
										</Select.Trigger>
										<Select.Content>
											{#each codeFiles as f}
												<Select.Item value={f.language}>{f.filename}</Select.Item>
											{/each}
										</Select.Content>
									</Select.Root>
								</div>
								<input hidden name={attrs.name} bind:value={$formData.language} />
							</Form.Control>
							<!-- <Form.Description>
								You can manage verified email addresses in your <a href="/examples/forms"
									>email settings</a
								>.
							</Form.Description> -->
							<Form.FieldErrors />
						</Form.Field>
					</div>
					<Form.Field {form} name="code" class="grow">
						<Form.Control let:attrs>
							<MonacoEditor class="h-full" bind:value={$formData.code} language={$formData.language}
							></MonacoEditor>
							<input hidden name={attrs.name} bind:value={$formData.code} />
						</Form.Control>
						<!-- <Form.Description>
								You can manage verified email addresses in your <a href="/examples/forms"
									>email settings</a
								>.
							</Form.Description> -->
						<Form.FieldErrors />
					</Form.Field>
					<!-- <div class="grid gap-4 py-4">
					<div class="grid grid-cols-4 items-center gap-4">
						<Label for="name" class="text-right">Name</Label>
						<Input id="name" value="Pedro Duarte" class="col-span-3" />
					</div>
					<div class="grid grid-cols-4 items-center gap-4">
						<Label for="username" class="text-right">Username</Label>
						<Input id="username" value="@peduarte" class="col-span-3" />
					</div>
				</div> -->
					<Dialog.Footer class="flex-end flex flex-none flex-row">
						{#if $timeout}
							<svg
								aria-hidden="true"
								class="h-8 w-8 animate-spin fill-blue-600 text-gray-200 dark:text-gray-600"
								viewBox="0 0 100 101"
								fill="none"
								xmlns="http://www.w3.org/2000/svg"
							>
								<path
									d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
									fill="currentColor"
								/>
								<path
									d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
									fill="currentFill"
								/>
							</svg>
						{/if}

						<Form.Button>
							{#if $delayed}
								<svg
									aria-hidden="true"
									class="mr-2 h-4 w-4 animate-spin fill-blue-600 text-gray-200 dark:text-gray-600"
									viewBox="0 0 100 101"
									fill="none"
									xmlns="http://www.w3.org/2000/svg"
								>
									<path
										d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
										fill="currentColor"
									/>
									<path
										d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
										fill="currentFill"
									/>
								</svg>
							{/if}
							Submit
						</Form.Button>
					</Dialog.Footer>

					{#if browser && dev}
						<SuperDebug data={$formData} />
		{#if accept.type === 'code'}
			<CodeSubmitCard data={form} languages={accept.languages}></CodeSubmitCard>
		{:else if accept.type === 'file'}
			<!-- else if content here -->
		{:else}
			<!-- else content here -->
		{/if}
				</form>
			</Dialog.Content>
		</Dialog.Root>
		<!-- <div
			class="-mx-2 flex items-center space-x-4 rounded-md p-2 transition-all hover:bg-accent hover:text-accent-foreground"
		>
+204 −0
Original line number Diff line number Diff line
<script lang="ts" context="module">
	import { z } from 'zod';
	import _ from 'lodash';

	const defaultCodeFilenames = [
		{
			language: 'cpp',
			filename: 'main.cpp'
		},
		{
			language: 'java',
			filename: 'Main.java'
		},
		{
			language: 'python',
			filename: 'main.py'
		}
	];

	const defaultDict = _.keyBy(defaultCodeFilenames, 'language');

	function getFilename(
		lang:
			| 'python'
			| 'cpp'
			| 'java'
			| {
					language: 'python' | 'cpp' | 'java';
					filename: string;
			  }
	): {
		language: 'python' | 'cpp' | 'java';
		filename: string;
	} {
		// Check if lang is an object
		if (typeof lang === 'object' && lang !== null && 'filename' in lang) {
			return {
				language: lang.language,
				filename: lang.filename
			};
		}
		// If lang is a string literal, return default name
		return {
			language: lang,
			filename: defaultDict[lang].filename
		};
	}

	export const submissionFormSchema = z.object({
		code: z.string(),
		language: languagesSchema,
		key: z.string().optional()
	});
	export type SubmissionFormSchema = typeof submissionFormSchema;
</script>

<script lang="ts">
	import { Upload } from 'svelte-radix';
	import { buttonVariants } from '$lib/components/ui/button';
	import * as Dialog from '$lib/components/ui/dialog';
	import * as Form from '$lib/components/ui/form';
	import MonacoEditor from '$lib/components/monaco/MonacoEditor.svelte';
	import * as Select from '$lib/components/ui/select';
	import SuperDebug from 'sveltekit-superforms';
	import { browser, dev } from '$app/environment';

	import { type SuperValidated, type Infer, superForm } from 'sveltekit-superforms';
	import { zodClient } from 'sveltekit-superforms/adapters';
	import { upload } from '$lib/shared/file';
	import { goto } from '$app/navigation';
	import { codeFilesSchema, languagesSchema } from '$lib/shared/submission';

	export let data: SuperValidated<Infer<SubmissionFormSchema>>;
	export let languages: z.infer<typeof codeFilesSchema>;

	const codeFilenameList = languages.map(getFilename);
	const codeFilenameDict = _.keyBy(codeFilenameList, 'language');
	if (codeFilenameList.length !== 0) {
		data.data.language = codeFilenameList[0].language;
	}

	const form = superForm(data, {
		dataType: 'json',
		async onSubmit({ jsonData }) {
			const blob = new Blob([$formData.code], { type: 'plain/text' });
			const file = new File([blob], codeFilenameDict[$formData.language].filename);
			const key = await upload(file);
			const data = Object.fromEntries(_.concat(_.entries($formData), [['key', key]]));
			jsonData(data);
		},
		validators: zodClient(submissionFormSchema),
		delayMs: 500,
		timeoutMs: 8000,
		applyAction: false,
		onResult({ result }) {
			if (result.type === 'redirect') {
				goto(result.location);
			}
		}
	});

	const { form: formData, enhance, delayed, timeout } = form;
	$: selectedLanguage = {
		label: codeFilenameDict[$formData.language].filename,
		value: $formData.language
	};
</script>

<Dialog.Root>
	<Dialog.Trigger class={buttonVariants({ variant: 'outline' })}>
		<Upload class="mr-2 h-4 w-4" />
		Create
	</Dialog.Trigger>
	<Dialog.Content class="h-[60%] sm:max-h-[80%] sm:max-w-[60%]">
		<form method="POST" class="flex flex-col" use:enhance>
			<Dialog.Header class="flex-none">
				<Dialog.Title>Create submission</Dialog.Title>
				<!-- <Dialog.Description>
						Make changes to your profile here. Click save when you're done.
					</Dialog.Description> -->
			</Dialog.Header>
			<div class="my-1">
				<Form.Field {form} name="language">
					<Form.Control let:attrs>
						<div class="flex flex-row items-center space-x-1">
							<Form.Label>Filename</Form.Label>
							<Select.Root
								selected={selectedLanguage}
								onSelectedChange={(s) => {
									s && ($formData.language = s.value);
								}}
							>
								<Select.Trigger class="w-[180px]">
									<Select.Value placeholder="File name" />
								</Select.Trigger>
								<Select.Content>
									{#each codeFilenameList as f}
										<Select.Item value={f.language}>{f.filename}</Select.Item>
									{/each}
								</Select.Content>
							</Select.Root>
						</div>
						<input hidden name={attrs.name} bind:value={$formData.language} />
					</Form.Control>
					<Form.FieldErrors />
				</Form.Field>
			</div>
			<Form.Field {form} name="code" class="grow">
				<Form.Control let:attrs>
					<MonacoEditor class="h-full" bind:value={$formData.code} language={$formData.language}
					></MonacoEditor>
					<input hidden name={attrs.name} bind:value={$formData.code} />
				</Form.Control>
				<Form.FieldErrors />
			</Form.Field>
			<Dialog.Footer class="flex-end flex flex-none flex-row">
				{#if $timeout}
					<svg
						aria-hidden="true"
						class="h-8 w-8 animate-spin fill-blue-600 text-gray-200 dark:text-gray-600"
						viewBox="0 0 100 101"
						fill="none"
						xmlns="http://www.w3.org/2000/svg"
					>
						<path
							d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
							fill="currentColor"
						/>
						<path
							d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
							fill="currentFill"
						/>
					</svg>
				{/if}

				<Form.Button>
					{#if $delayed}
						<svg
							aria-hidden="true"
							class="mr-2 h-4 w-4 animate-spin fill-blue-600 text-gray-200 dark:text-gray-600"
							viewBox="0 0 100 101"
							fill="none"
							xmlns="http://www.w3.org/2000/svg"
						>
							<path
								d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
								fill="currentColor"
							/>
							<path
								d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
								fill="currentFill"
							/>
						</svg>
					{/if}
					Submit
				</Form.Button>
			</Dialog.Footer>

			{#if browser && dev}
				<SuperDebug data={$formData} />
			{/if}
		</form>
	</Dialog.Content>
</Dialog.Root>
Loading