refactor(guides): replace NewGuide with unified GuideForm

- Merge useNewGuide and NewGuide into useGuideForm and GuideForm
  to handle both create and edit flows
- Add /edit-guide/:id route pointing to GuideForm
- Replace Edit button in Guide view with Link to edit route
- Add defaultValue to Frame title input for edit pre-population
- Delete NewGuide.tsx, NewGuide.css, useNewGuide.ts
This commit is contained in:
johannes-hernehult 2026-06-17 20:05:01 +02:00
parent 4dd8ce8b17
commit ea6adc236e
6 changed files with 79 additions and 58 deletions

View file

@ -52,6 +52,7 @@ export default function Frame({
Title Title
<input <input
type="text" type="text"
defaultValue={frame.title}
onChange={(e) => updateTitle(frame.id, e.target.value)} onChange={(e) => updateTitle(frame.id, e.target.value)}
/> />
</label> </label>

View file

@ -7,19 +7,36 @@ import {
import { useIDB } from "../../../lib/contexts/dbinit.context"; import { useIDB } from "../../../lib/contexts/dbinit.context";
import { useGuides } from "../../../lib/contexts/guide.context"; import { useGuides } from "../../../lib/contexts/guide.context";
export default function useNewGuide() { function makeEmptyGuide(): GuideType {
const { db } = useIDB(); return {
const { setGuides } = useGuides();
const [guide, setGuide] = useState<GuideType>({
id: crypto.randomUUID(), id: crypto.randomUUID(),
title: "", title: "",
description: "", description: "",
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
deletedAt: undefined, deletedAt: undefined,
}); };
const [frames, setFrames] = useState<FrameType[]>([]); }
export default function useGuideForm(initial?: CompiledGuideType) {
const { db } = useIDB();
const { setGuides } = useGuides();
const isEditing = Boolean(initial);
const [guide, setGuide] = useState<GuideType>(
initial
? {
id: initial.id,
title: initial.title,
description: initial.description,
createdAt: initial.createdAt,
updatedAt: initial.updatedAt,
deletedAt: initial.deletedAt,
}
: makeEmptyGuide(),
);
const [frames, setFrames] = useState<FrameType[]>(initial?.frames ?? []);
const handleUpdateTitle = (title: string) => { const handleUpdateTitle = (title: string) => {
setGuide((prev) => ({ ...prev, title })); setGuide((prev) => ({ ...prev, title }));
@ -32,20 +49,16 @@ export default function useNewGuide() {
const handleAddFrames = (e: React.ChangeEvent<HTMLInputElement>) => { const handleAddFrames = (e: React.ChangeEvent<HTMLInputElement>) => {
const newFiles = e.target.files; const newFiles = e.target.files;
if (!newFiles) return; if (!newFiles) return;
const newFrameObjects = Array.from(newFiles).map((file) => {
const newFilesArray = Array.from(newFiles);
const newFrameObjects = newFilesArray.map((file) => {
const frameId = crypto.randomUUID(); const frameId = crypto.randomUUID();
return { return {
id: frameId, id: frameId,
file: file, file,
title: "", title: "",
guideId: guide.id, guideId: guide.id,
steps: [{ id: crypto.randomUUID(), text: "", frameId: frameId }], steps: [{ id: crypto.randomUUID(), text: "", frameId }],
}; };
}); });
setFrames((prev) => [...prev, ...newFrameObjects]); setFrames((prev) => [...prev, ...newFrameObjects]);
}; };
@ -76,7 +89,7 @@ export default function useNewGuide() {
...frame, ...frame,
steps: [ steps: [
...frame.steps, ...frame.steps,
{ id: crypto.randomUUID(), text: "", frameId: frameId }, { id: crypto.randomUUID(), text: "", frameId },
], ],
} }
: frame, : frame,
@ -86,58 +99,45 @@ export default function useNewGuide() {
const handleUpdateStep = (stepId: string, text: string) => { const handleUpdateStep = (stepId: string, text: string) => {
setFrames((prev) => setFrames((prev) =>
prev.map((frame) => prev.map((frame) => ({
frame.steps ...frame,
? { steps: frame.steps.map((step) =>
...frame, step.id === stepId ? { ...step, text } : step,
steps: frame.steps.map((step) => ),
step.id === stepId ? { ...step, text } : step, })),
),
}
: frame,
),
); );
}; };
const handleRemoveStep = (stepId: string) => { const handleRemoveStep = (stepId: string) => {
setFrames((prev) => setFrames((prev) =>
prev.map((frame) => prev.map((frame) => ({
frame.steps ...frame,
? { steps: frame.steps.filter((step) => step.id !== stepId),
...frame, })),
steps: frame.steps.filter((step) => step.id !== stepId),
}
: frame,
),
); );
}; };
const handleSaveGuide = async (e: React.SubmitEvent<HTMLFormElement>) => { const handleSaveGuide = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
if (!db) return;
const compiledGuide: CompiledGuideType = { const compiledGuide: CompiledGuideType = {
id: guide.id, ...guide,
title: guide.title, updatedAt: new Date(),
description: guide.description,
createdAt: guide.createdAt,
updatedAt: guide.updatedAt,
deletedAt: guide.deletedAt,
frames, frames,
}; };
if (!db) return;
await db.put("guides", compiledGuide); await db.put("guides", compiledGuide);
setGuide({
id: crypto.randomUUID(),
title: "",
description: "",
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: undefined,
});
setFrames([]);
setGuides((prev) => [compiledGuide, ...prev]); if (isEditing) {
setGuides((prev) =>
prev.map((g) => (g.id === compiledGuide.id ? compiledGuide : g)),
);
} else {
setGuides((prev) => [compiledGuide, ...prev]);
setGuide(makeEmptyGuide());
setFrames([]);
}
}; };
return { return {
@ -145,6 +145,7 @@ export default function useNewGuide() {
setGuide, setGuide,
frames, frames,
setFrames, setFrames,
isEditing,
handleUpdateTitle, handleUpdateTitle,
handleUpdateDescription, handleUpdateDescription,
handleAddFrames, handleAddFrames,
@ -157,3 +158,7 @@ export default function useNewGuide() {
handleSaveGuide, handleSaveGuide,
}; };
} }
/* */
/* Figure out how to use this hook for both editing and creating. */
/* */

View file

@ -1,6 +1,7 @@
import { useParams } from "react-router"; import { useParams } from "react-router";
import { useGuides } from "../../../lib/contexts/guide.context"; import { useGuides } from "../../../lib/contexts/guide.context";
import useGuide from "../hooks/useGuide"; import useGuide from "../hooks/useGuide";
import { Link } from "react-router";
export default function Guide() { export default function Guide() {
const { id } = useParams(); const { id } = useParams();
@ -31,7 +32,7 @@ export default function Guide() {
</div> </div>
<button onClick={() => deleteGuide(guide.id)}>Delete</button> <button onClick={() => deleteGuide(guide.id)}>Delete</button>
<button>Edit</button> <Link to={`/edit-guide/${guide.id}`}>Edit</Link>
<button onClick={exportGuide}>Export PDF</button> <button onClick={exportGuide}>Export PDF</button>
</div> </div>
); );

View file

@ -1,10 +1,17 @@
import "./NewGuide.css"; import "./GuideForm.css";
import FileUpload from "../components/FileUpload"; import FileUpload from "../components/FileUpload";
import useNewGuide from "../hooks/useNewGuide"; import useGuideForm from "../hooks/useGuideForm";
import Frame from "../components/Frame"; import Frame from "../components/Frame";
import { useParams } from "react-router";
import { useGuides } from "../../../lib/contexts/guide.context";
export default function GuideForm() {
const { id } = useParams();
const { guides } = useGuides();
const g = guides.find((g) => g.id === id);
export default function NewGuide() {
const { const {
guide,
frames, frames,
handleUpdateTitle, handleUpdateTitle,
handleUpdateDescription, handleUpdateDescription,
@ -16,7 +23,8 @@ export default function NewGuide() {
handleRemoveStep, handleRemoveStep,
handleUpdateStep, handleUpdateStep,
handleSaveGuide, handleSaveGuide,
} = useNewGuide(); isEditing,
} = useGuideForm(g);
return ( return (
<div className="new-guide"> <div className="new-guide">
@ -26,6 +34,7 @@ export default function NewGuide() {
<input <input
type="text" type="text"
name="title" name="title"
defaultValue={guide?.title}
onChange={(e) => handleUpdateTitle(e.target.value)} onChange={(e) => handleUpdateTitle(e.target.value)}
/> />
</label> </label>
@ -33,6 +42,7 @@ export default function NewGuide() {
<span>Description</span> <span>Description</span>
<textarea <textarea
name="description" name="description"
defaultValue={guide?.description}
onChange={(e) => handleUpdateDescription(e.target.value)} onChange={(e) => handleUpdateDescription(e.target.value)}
/> />
</label> </label>

View file

@ -1,6 +1,6 @@
import { createBrowserRouter } from "react-router"; import { createBrowserRouter } from "react-router";
import App from "./App.tsx"; import App from "./App.tsx";
import NewGuide from "./features/guides/routes/NewGuide.tsx"; import GuideForm from "./features/guides/routes/GuideForm.tsx";
import NotFound from "./components/not-found/NotFound.tsx"; import NotFound from "./components/not-found/NotFound.tsx";
import Guide from "./features/guides/routes/Guide.tsx"; import Guide from "./features/guides/routes/Guide.tsx";
@ -11,12 +11,16 @@ export const router = createBrowserRouter([
children: [ children: [
{ {
path: "/new-guide", path: "/new-guide",
Component: NewGuide, Component: GuideForm,
}, },
{ {
path: "/guide/:id", path: "/guide/:id",
Component: Guide, Component: Guide,
}, },
{
path: "/edit-guide/:id",
Component: GuideForm,
},
], ],
}, },
{ {