feat(guides): scaffold new guide form, hook, and update schemas

- Add useNewGuide hook with state and handlers for guide, frames, and steps
- Add title, description, and file upload inputs to NewGuide
- Add FileUpload component and supporting hooks
- Add guideId to FrameSchema, remove steps and order
- Remove content field and frames array from GuideSchema
- Remove order field from StepsSchema
This commit is contained in:
johannes-hernehult 2026-06-10 20:46:06 +02:00
parent fc63290806
commit 8a8d495432
4 changed files with 97 additions and 6 deletions

View file

@ -0,0 +1,8 @@
export default function FileUpload() {
return (
<label>
<span>File</span>
<input type="file" name="file" accept="images/*" multiple />
</label>
);
}

View file

@ -0,0 +1,74 @@
import { useState } from "react";
import { Frame, type Guide, type Steps } from "../../../lib/schemas";
export default function useNewGuide() {
const [guide, setGuide] = useState<Guide>({
id: crypto.randomUUID(),
title: "",
description: "",
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: undefined,
});
const [frames, setFrames] = useState<Frame[]>([]);
const [steps, setSteps] = useState<Steps[]>([]);
const handleUpdateTitle = (title: string) => {
setGuide((prev) => ({ ...prev, title }));
};
const handleUpdateDescription = (description: string) => {
setGuide((prev) => ({ ...prev, description }));
};
const handleAddFrames = (newFiles: File[]) => {
const newFrameObjects = newFiles.map((file) => ({
id: crypto.randomUUID(),
file: file,
guideId: guide.id,
}));
setFrames((prev) => [...prev, ...newFrameObjects]);
};
const handleRemoveFrame = (frameId: string) => {
setFrames((prev) => prev.filter((frame) => frame.id !== frameId));
};
const handleAddSteps = (frameId: string) => {
setSteps((prev) => [
...prev,
{
id: crypto.randomUUID(),
text: "",
frameId: frameId,
},
]);
};
const handleUpdateStepText = (stepId: string, text: string) => {
setSteps((prev) =>
prev.map((step) => (step.id === stepId ? { ...step, text } : step)),
);
};
const handleRemoveStep = (stepId: string) => {
setSteps((prev) => prev.filter((step) => step.id !== stepId));
};
return {
guide,
setGuide,
frames,
setFrames,
steps,
setSteps,
handleUpdateTitle,
handleUpdateDescription,
handleAddFrames,
handleAddSteps,
handleRemoveFrame,
handleUpdateStepText,
handleRemoveStep,
};
}

View file

@ -1,7 +1,20 @@
import FileUpload from "../components/FileUpload";
export default function NewGuide() {
return (
<div>
<h1>New Guide</h1>
<form>
<label>
<span>Title</span>
<input type="text" name="title" />
</label>
<label>
<span>Description</span>
<textarea name="description" />
</label>
<FileUpload />
</form>
</div>
);
}

View file

@ -3,26 +3,22 @@ import { z } from "zod";
export const StepsSchema = z.object({
id: z.uuid().default(() => crypto.randomUUID()),
text: z.string().min(1, "Text is required"),
order: z.number(),
frameId: z.uuid(),
});
export const FrameSchema = z.object({
id: z.uuid().default(() => crypto.randomUUID()),
file: z.instanceof(File),
steps: z.array(StepsSchema).min(1, "Steps are required"),
order: z.number(),
guideId: z.uuid(),
});
export const GuideSchema = z.object({
id: z.uuid().default(() => crypto.randomUUID()),
title: z.string().min(1, "Title is required"),
description: z.string().optional(),
content: z.string().optional(),
createdAt: z.date(),
updatedAt: z.date(),
deletedAt: z.date().optional(),
frames: z.array(FrameSchema).min(1, "Frames are required"),
});
export type Steps = z.infer<typeof StepsSchema>;