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
<input
type="text"
defaultValue={frame.title}
onChange={(e) => updateTitle(frame.id, e.target.value)}
/>
</label>

View file

@ -7,19 +7,36 @@ import {
import { useIDB } from "../../../lib/contexts/dbinit.context";
import { useGuides } from "../../../lib/contexts/guide.context";
export default function useNewGuide() {
const { db } = useIDB();
const { setGuides } = useGuides();
const [guide, setGuide] = useState<GuideType>({
function makeEmptyGuide(): GuideType {
return {
id: crypto.randomUUID(),
title: "",
description: "",
createdAt: new Date(),
updatedAt: new Date(),
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) => {
setGuide((prev) => ({ ...prev, title }));
@ -32,20 +49,16 @@ export default function useNewGuide() {
const handleAddFrames = (e: React.ChangeEvent<HTMLInputElement>) => {
const newFiles = e.target.files;
if (!newFiles) return;
const newFilesArray = Array.from(newFiles);
const newFrameObjects = newFilesArray.map((file) => {
const newFrameObjects = Array.from(newFiles).map((file) => {
const frameId = crypto.randomUUID();
return {
id: frameId,
file: file,
file,
title: "",
guideId: guide.id,
steps: [{ id: crypto.randomUUID(), text: "", frameId: frameId }],
steps: [{ id: crypto.randomUUID(), text: "", frameId }],
};
});
setFrames((prev) => [...prev, ...newFrameObjects]);
};
@ -76,7 +89,7 @@ export default function useNewGuide() {
...frame,
steps: [
...frame.steps,
{ id: crypto.randomUUID(), text: "", frameId: frameId },
{ id: crypto.randomUUID(), text: "", frameId },
],
}
: frame,
@ -86,58 +99,45 @@ export default function useNewGuide() {
const handleUpdateStep = (stepId: string, text: string) => {
setFrames((prev) =>
prev.map((frame) =>
frame.steps
? {
...frame,
steps: frame.steps.map((step) =>
step.id === stepId ? { ...step, text } : step,
),
}
: frame,
),
prev.map((frame) => ({
...frame,
steps: frame.steps.map((step) =>
step.id === stepId ? { ...step, text } : step,
),
})),
);
};
const handleRemoveStep = (stepId: string) => {
setFrames((prev) =>
prev.map((frame) =>
frame.steps
? {
...frame,
steps: frame.steps.filter((step) => step.id !== stepId),
}
: frame,
),
prev.map((frame) => ({
...frame,
steps: frame.steps.filter((step) => step.id !== stepId),
})),
);
};
const handleSaveGuide = async (e: React.SubmitEvent<HTMLFormElement>) => {
const handleSaveGuide = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!db) return;
const compiledGuide: CompiledGuideType = {
id: guide.id,
title: guide.title,
description: guide.description,
createdAt: guide.createdAt,
updatedAt: guide.updatedAt,
deletedAt: guide.deletedAt,
...guide,
updatedAt: new Date(),
frames,
};
if (!db) return;
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 {
@ -145,6 +145,7 @@ export default function useNewGuide() {
setGuide,
frames,
setFrames,
isEditing,
handleUpdateTitle,
handleUpdateDescription,
handleAddFrames,
@ -157,3 +158,7 @@ export default function useNewGuide() {
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 { useGuides } from "../../../lib/contexts/guide.context";
import useGuide from "../hooks/useGuide";
import { Link } from "react-router";
export default function Guide() {
const { id } = useParams();
@ -31,7 +32,7 @@ export default function Guide() {
</div>
<button onClick={() => deleteGuide(guide.id)}>Delete</button>
<button>Edit</button>
<Link to={`/edit-guide/${guide.id}`}>Edit</Link>
<button onClick={exportGuide}>Export PDF</button>
</div>
);

View file

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

View file

@ -1,6 +1,6 @@
import { createBrowserRouter } from "react-router";
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 Guide from "./features/guides/routes/Guide.tsx";
@ -11,12 +11,16 @@ export const router = createBrowserRouter([
children: [
{
path: "/new-guide",
Component: NewGuide,
Component: GuideForm,
},
{
path: "/guide/:id",
Component: Guide,
},
{
path: "/edit-guide/:id",
Component: GuideForm,
},
],
},
{