refactor(guides): colocate steps within frames state
- Remove standalone steps state; steps now live inside each Frame object - Initialize each frame with one default empty step on file upload - Update handleAddSteps, handleUpdateStep, handleRemoveStep to operate on frames array via nested map/filter - Rename handleUpdateStepText -> handleUpdateStep - Rename schema types to StepType, FrameType, GuideType - Add steps and title fields to FrameSchema - Wire FileUpload onChange and render Frame components in NewGuide - Add NewGuide.css and basic input/textarea border styles
This commit is contained in:
parent
d1df5b16ed
commit
1bb6ddf403
8 changed files with 193 additions and 31 deletions
|
|
@ -1,8 +1,18 @@
|
|||
export default function FileUpload() {
|
||||
export default function FileUpload({
|
||||
onChange,
|
||||
}: {
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}) {
|
||||
return (
|
||||
<label>
|
||||
<span>File</span>
|
||||
<input type="file" name="file" accept="images/*" multiple />
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
accept="images/*"
|
||||
multiple
|
||||
onChange={(e) => onChange(e)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
33
src/features/guides/components/Frame.tsx
Normal file
33
src/features/guides/components/Frame.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import type { FrameType, StepType } from "../../../lib/schemas";
|
||||
import Step from "./Step";
|
||||
|
||||
export default function Frame({
|
||||
frame,
|
||||
onRemove,
|
||||
addStep,
|
||||
removeStep,
|
||||
updateStep,
|
||||
}: {
|
||||
frame: FrameType;
|
||||
onRemove: (frameId: string) => void;
|
||||
addStep: (frameId: string) => void;
|
||||
removeStep: (stepId: string) => void;
|
||||
updateStep: (stepId: string, text: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => onRemove(frame.id)}>Remove Frame</button>
|
||||
<img src={URL.createObjectURL(frame.file)} alt={frame.title} />
|
||||
<button onClick={() => addStep(frame.id)}>Add Step</button>
|
||||
{frame.steps.map((step: StepType, index: number) => (
|
||||
<Step
|
||||
key={step.id}
|
||||
step={step}
|
||||
index={index}
|
||||
removeStep={removeStep}
|
||||
updateStep={updateStep}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
src/features/guides/components/Step.tsx
Normal file
27
src/features/guides/components/Step.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import type { StepType } from "../../../lib/schemas";
|
||||
|
||||
export default function Step({
|
||||
step,
|
||||
removeStep,
|
||||
updateStep,
|
||||
index,
|
||||
}: {
|
||||
step: StepType;
|
||||
removeStep: (stepId: string) => void;
|
||||
updateStep: (stepId: string, text: string) => void;
|
||||
index: number;
|
||||
}) {
|
||||
return (
|
||||
<div className={"step " + index}>
|
||||
<label>
|
||||
<span>{index + 1}. </span>
|
||||
<input
|
||||
type="text"
|
||||
value={step.text}
|
||||
onChange={(e) => updateStep(step.id, e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button onClick={() => removeStep(step.id)}>Remove Step</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ export default function useNewGuide() {
|
|||
deletedAt: undefined,
|
||||
});
|
||||
const [frames, setFrames] = useState<Frame[]>([]);
|
||||
const [steps, setSteps] = useState<Steps[]>([]);
|
||||
|
||||
const handleUpdateTitle = (title: string) => {
|
||||
setGuide((prev) => ({ ...prev, title }));
|
||||
|
|
@ -21,12 +20,21 @@ export default function useNewGuide() {
|
|||
setGuide((prev) => ({ ...prev, description }));
|
||||
};
|
||||
|
||||
const handleAddFrames = (newFiles: File[]) => {
|
||||
const newFrameObjects = newFiles.map((file) => ({
|
||||
id: crypto.randomUUID(),
|
||||
file: file,
|
||||
guideId: guide.id,
|
||||
}));
|
||||
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 frameId = crypto.randomUUID();
|
||||
return {
|
||||
id: frameId,
|
||||
file: file,
|
||||
guideId: guide.id,
|
||||
steps: [{ id: crypto.randomUUID(), text: "", frameId: frameId }],
|
||||
};
|
||||
});
|
||||
|
||||
setFrames((prev) => [...prev, ...newFrameObjects]);
|
||||
};
|
||||
|
|
@ -36,31 +44,53 @@ export default function useNewGuide() {
|
|||
};
|
||||
|
||||
const handleAddSteps = (frameId: string) => {
|
||||
setSteps((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
text: "",
|
||||
frameId: frameId,
|
||||
},
|
||||
]);
|
||||
setFrames((prev) =>
|
||||
prev.map((frame) =>
|
||||
frame.id === frameId
|
||||
? {
|
||||
...frame,
|
||||
steps: [
|
||||
...frame.steps,
|
||||
{ id: crypto.randomUUID(), text: "", frameId: frameId },
|
||||
],
|
||||
}
|
||||
: frame,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleUpdateStepText = (stepId: string, text: string) => {
|
||||
setSteps((prev) =>
|
||||
prev.map((step) => (step.id === stepId ? { ...step, text } : step)),
|
||||
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,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveStep = (stepId: string) => {
|
||||
setSteps((prev) => prev.filter((step) => step.id !== stepId));
|
||||
setFrames((prev) =>
|
||||
prev.map((frame) =>
|
||||
frame.steps
|
||||
? {
|
||||
...frame,
|
||||
steps: frame.steps.filter((step) => step.id !== stepId),
|
||||
}
|
||||
: frame,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleSaveGuide = () => {
|
||||
const compiledGuide = {
|
||||
...guide,
|
||||
frames,
|
||||
steps,
|
||||
};
|
||||
|
||||
console.log(compiledGuide);
|
||||
|
|
@ -71,14 +101,12 @@ export default function useNewGuide() {
|
|||
setGuide,
|
||||
frames,
|
||||
setFrames,
|
||||
steps,
|
||||
setSteps,
|
||||
handleUpdateTitle,
|
||||
handleUpdateDescription,
|
||||
handleAddFrames,
|
||||
handleAddSteps,
|
||||
handleRemoveFrame,
|
||||
handleUpdateStepText,
|
||||
handleUpdateStep,
|
||||
handleRemoveStep,
|
||||
handleSaveGuide,
|
||||
};
|
||||
|
|
|
|||
19
src/features/guides/routes/NewGuide.css
Normal file
19
src/features/guides/routes/NewGuide.css
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
.new-guide {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.new-guide__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
|
||||
width: 100%;
|
||||
max-width: 540px;
|
||||
}
|
||||
|
||||
.new-guide__form label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -1,9 +1,28 @@
|
|||
import "./NewGuide.css";
|
||||
import FileUpload from "../components/FileUpload";
|
||||
import useNewGuide from "../hooks/useNewGuide";
|
||||
import Frame from "../components/Frame";
|
||||
|
||||
export default function NewGuide() {
|
||||
const {
|
||||
frames,
|
||||
handleUpdateTitle,
|
||||
handleUpdateDescription,
|
||||
handleAddFrames,
|
||||
handleAddSteps,
|
||||
handleRemoveFrame,
|
||||
handleRemoveStep,
|
||||
handleUpdateStep,
|
||||
handleSaveGuide,
|
||||
} = useNewGuide();
|
||||
const handleSubmitForm = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
console.log(e.currentTarget);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form>
|
||||
<div className="new-guide">
|
||||
<form className="new-guide__form" onSubmit={handleSubmitForm}>
|
||||
<label>
|
||||
<span>Title</span>
|
||||
<input type="text" name="title" />
|
||||
|
|
@ -13,7 +32,19 @@ export default function NewGuide() {
|
|||
<textarea name="description" />
|
||||
</label>
|
||||
|
||||
<FileUpload />
|
||||
<FileUpload onChange={handleAddFrames} />
|
||||
|
||||
{frames.length > 0 &&
|
||||
frames.map((frame, index) => (
|
||||
<Frame
|
||||
key={index}
|
||||
frame={frame}
|
||||
onRemove={handleRemoveFrame}
|
||||
addStep={handleAddSteps}
|
||||
removeStep={handleRemoveStep}
|
||||
updateStep={handleUpdateStep}
|
||||
/>
|
||||
))}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ export const StepsSchema = z.object({
|
|||
|
||||
export const FrameSchema = z.object({
|
||||
id: z.uuid().default(() => crypto.randomUUID()),
|
||||
title: z.string().min(1, "Title is required"),
|
||||
file: z.instanceof(File),
|
||||
guideId: z.uuid(),
|
||||
steps: z.array(StepsSchema),
|
||||
});
|
||||
|
||||
export const GuideSchema = z.object({
|
||||
|
|
@ -21,6 +23,6 @@ export const GuideSchema = z.object({
|
|||
deletedAt: z.date().optional(),
|
||||
});
|
||||
|
||||
export type Steps = z.infer<typeof StepsSchema>;
|
||||
export type Frame = z.infer<typeof FrameSchema>;
|
||||
export type Guide = z.infer<typeof GuideSchema>;
|
||||
export type StepType = z.infer<typeof StepsSchema>;
|
||||
export type FrameType = z.infer<typeof FrameSchema>;
|
||||
export type GuideType = z.infer<typeof GuideSchema>;
|
||||
|
|
|
|||
|
|
@ -68,3 +68,15 @@ textarea {
|
|||
font-size: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
border: 3px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
height: 5rem;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue