mirror of
https://github.com/massbug/judge4c.git
synced 2025-05-17 23:12:23 +00:00
feat(problemset): add metadata, description, and solution forms for new problem creation
This commit is contained in:
parent
ba676b3213
commit
424f24694b
@ -0,0 +1,142 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import DockView from "@/components/dockview";
|
||||||
|
import { ArrowRightIcon } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import MdxPreview from "@/components/mdx-preview";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import MarkdownEditor from "@/components/markdown-editor";
|
||||||
|
import { useNewProblemStore } from "@/app/(app)/dashboard/@admin/problemset/new/store";
|
||||||
|
import { problemSchema } from "@/components/features/dashboard/admin/problemset/new/schema";
|
||||||
|
import { newProblemMetadataSchema } from "@/components/features/dashboard/admin/problemset/new/components/metadata-form";
|
||||||
|
|
||||||
|
export const newProblemDescriptionSchema = problemSchema.pick({
|
||||||
|
description: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
type NewProblemDescriptionSchema = z.infer<
|
||||||
|
typeof newProblemDescriptionSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default function NewProblemDescriptionForm() {
|
||||||
|
const {
|
||||||
|
hydrated,
|
||||||
|
displayId,
|
||||||
|
title,
|
||||||
|
difficulty,
|
||||||
|
published,
|
||||||
|
description,
|
||||||
|
setData,
|
||||||
|
} = useNewProblemStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const form = useForm<NewProblemDescriptionSchema>({
|
||||||
|
resolver: zodResolver(newProblemDescriptionSchema),
|
||||||
|
defaultValues: {
|
||||||
|
description: description || "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data: NewProblemDescriptionSchema) => {
|
||||||
|
setData(data);
|
||||||
|
router.push("/dashboard/problemset/new/solution");
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hydrated) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
newProblemMetadataSchema.parse({
|
||||||
|
displayId,
|
||||||
|
title,
|
||||||
|
difficulty,
|
||||||
|
published,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
router.push("/dashboard/problemset/new/metadata");
|
||||||
|
}
|
||||||
|
}, [difficulty, displayId, hydrated, published, router, title]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="h-full space-y-8 p-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="h-full flex flex-col">
|
||||||
|
<FormLabel className="flex flex-none items-center justify-between">
|
||||||
|
<span>Description</span>
|
||||||
|
<Button className="h-8 w-auto" type="submit">
|
||||||
|
Next
|
||||||
|
<ArrowRightIcon size={16} aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<DockView
|
||||||
|
storageKey="dockview:new-problem"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
id: "MdxPreview",
|
||||||
|
title: "Mdx Preview",
|
||||||
|
component: "MdxPreview",
|
||||||
|
tabComponent: "MdxPreview",
|
||||||
|
icon: "FileTextIcon",
|
||||||
|
node: (
|
||||||
|
<div className="h-full border-x border-muted relative">
|
||||||
|
<div className="absolute h-full w-full">
|
||||||
|
<ScrollArea className="h-full">
|
||||||
|
<MdxPreview
|
||||||
|
source={field.value}
|
||||||
|
className="p-4 md:p-6"
|
||||||
|
/>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "MarkdownEditor",
|
||||||
|
title: "Markdown Editor",
|
||||||
|
component: "MarkdownEditor",
|
||||||
|
tabComponent: "MarkdownEditor",
|
||||||
|
icon: "FileTextIcon",
|
||||||
|
node: (
|
||||||
|
<MarkdownEditor
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
position: { referencePanel: "MdxPreview", direction: "right" },
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
This is your problem description.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Difficulty } from "@/generated/client";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { getDifficultyColorClass } from "@/lib/utils";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNewProblemStore } from "@/app/(app)/dashboard/@admin/problemset/new/store";
|
||||||
|
import { problemSchema } from "@/components/features/dashboard/admin/problemset/new/schema";
|
||||||
|
|
||||||
|
export const newProblemMetadataSchema = problemSchema.pick({
|
||||||
|
displayId: true,
|
||||||
|
title: true,
|
||||||
|
difficulty: true,
|
||||||
|
published: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
type NewProblemMetadataSchema = z.infer<typeof newProblemMetadataSchema>;
|
||||||
|
|
||||||
|
export default function NewProblemMetadataForm() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { displayId, title, difficulty, published, setData } = useNewProblemStore();
|
||||||
|
|
||||||
|
const form = useForm<NewProblemMetadataSchema>({
|
||||||
|
resolver: zodResolver(newProblemMetadataSchema),
|
||||||
|
defaultValues: {
|
||||||
|
// displayId must be a number and cannot be an empty string ("")
|
||||||
|
// so set it to undefined here and convert it to "" in the Input component.
|
||||||
|
displayId: displayId || undefined,
|
||||||
|
title: title || "",
|
||||||
|
difficulty: difficulty || "EASY",
|
||||||
|
published: published || false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data: NewProblemMetadataSchema) => {
|
||||||
|
setData(data);
|
||||||
|
router.push("/dashboard/problemset/new/description");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="max-w-3xl mx-auto space-y-8 py-10"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="displayId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Display ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="e.g., 1001" {...field} value={field.value ?? ""} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Unique numeric identifier visible to users
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Problem Title</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="e.g., Two Sum" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Descriptive title summarizing the problem
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="difficulty"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Difficulty Level</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select difficulty level" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.values(Difficulty).map((difficulty) => (
|
||||||
|
<SelectItem key={difficulty} value={difficulty}>
|
||||||
|
<span className={getDifficultyColorClass(difficulty)}>
|
||||||
|
{difficulty}
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Categorize problem complexity for better filtering
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="published"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5 mr-2">
|
||||||
|
<FormLabel>Publish Status</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Make problem visible in public listings
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit">Next</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useNewProblemStore } from "@/app/(app)/dashboard/@admin/problemset/new/store";
|
||||||
|
import { problemSchema } from "@/components/features/dashboard/admin/problemset/new/schema";
|
||||||
|
import { newProblemMetadataSchema } from "@/components/features/dashboard/admin/problemset/new/components/metadata-form";
|
||||||
|
import { newProblemDescriptionSchema } from "@/components/features/dashboard/admin/problemset/new/components/description-form";
|
||||||
|
|
||||||
|
const newProblemSolutionSchema = problemSchema.pick({
|
||||||
|
solution: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
type NewProblemSolutionSchema = z.infer<typeof newProblemSolutionSchema>;
|
||||||
|
|
||||||
|
export default function NewProblemSolutionForm() {
|
||||||
|
const {
|
||||||
|
hydrated,
|
||||||
|
displayId,
|
||||||
|
title,
|
||||||
|
difficulty,
|
||||||
|
published,
|
||||||
|
description,
|
||||||
|
solution,
|
||||||
|
} = useNewProblemStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const form = useForm<NewProblemSolutionSchema>({
|
||||||
|
resolver: zodResolver(newProblemSolutionSchema),
|
||||||
|
defaultValues: {
|
||||||
|
solution: solution || "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data: NewProblemSolutionSchema) => {
|
||||||
|
console.log({
|
||||||
|
...data,
|
||||||
|
displayId,
|
||||||
|
title,
|
||||||
|
difficulty,
|
||||||
|
published,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hydrated) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
newProblemMetadataSchema.parse({
|
||||||
|
displayId,
|
||||||
|
title,
|
||||||
|
difficulty,
|
||||||
|
published,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
router.push("/dashboard/problemset/new/metadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
newProblemDescriptionSchema.parse({ description });
|
||||||
|
} catch {
|
||||||
|
router.push("/dashboard/problemset/new/description");
|
||||||
|
}
|
||||||
|
}, [hydrated, displayId, title, difficulty, published, description, router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="max-w-3xl mx-auto space-y-8 py-10"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="solution"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Solution</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
This is your problem solution.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit">Next</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user