fix(guides): fix hard reload empty state and frame rendering bugs
- Split GuideForm into outer/inner components to gate rendering on guide context loading, fixing empty edit form on hard reload - Add loading state to GuideContext - Replace URL.createObjectURL in render with useMemo + useEffect cleanup to fix memory leak and empty src warning - Fix frame reorder bug by keying Frame on frame.id instead of index
This commit is contained in:
parent
ea6adc236e
commit
eb30a325db
4 changed files with 35 additions and 24 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import "./Frame.css";
|
||||
import type { FrameType, StepType } from "../../../lib/schemas";
|
||||
import Step from "./Step";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
export default function Frame({
|
||||
frame,
|
||||
|
|
@ -21,6 +22,15 @@ export default function Frame({
|
|||
updateTitle: (frameId: string, title: string) => void;
|
||||
reOrder: (startIndex: number, endIndex: number) => void;
|
||||
}) {
|
||||
const objectUrl = useMemo(
|
||||
() => URL.createObjectURL(frame.file),
|
||||
[frame.file],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => URL.revokeObjectURL(objectUrl);
|
||||
}, [objectUrl]);
|
||||
|
||||
return (
|
||||
<div className="frame">
|
||||
<div className="buttons">
|
||||
|
|
@ -38,7 +48,6 @@ export default function Frame({
|
|||
>
|
||||
Down
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="remove"
|
||||
|
|
@ -47,7 +56,6 @@ export default function Frame({
|
|||
Remove Frame
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Title
|
||||
<input
|
||||
|
|
@ -56,7 +64,7 @@ export default function Frame({
|
|||
onChange={(e) => updateTitle(frame.id, e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<img src={URL.createObjectURL(frame.file)} alt={frame.title} />
|
||||
<img src={objectUrl} alt={frame.title} />
|
||||
<button
|
||||
type="button"
|
||||
className="add-step"
|
||||
|
|
@ -64,7 +72,6 @@ export default function Frame({
|
|||
>
|
||||
Add Step
|
||||
</button>
|
||||
|
||||
<div className="steps">
|
||||
{frame.steps.map((step: StepType, index: number) => (
|
||||
<Step
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
type CompiledGuideType,
|
||||
type FrameType,
|
||||
|
|
|
|||
|
|
@ -4,12 +4,9 @@ import useGuideForm from "../hooks/useGuideForm";
|
|||
import Frame from "../components/Frame";
|
||||
import { useParams } from "react-router";
|
||||
import { useGuides } from "../../../lib/contexts/guide.context";
|
||||
import type { CompiledGuideType } from "../../../lib/schemas";
|
||||
|
||||
export default function GuideForm() {
|
||||
const { id } = useParams();
|
||||
const { guides } = useGuides();
|
||||
const g = guides.find((g) => g.id === id);
|
||||
|
||||
function GuideFormInner({ initial }: { initial?: CompiledGuideType }) {
|
||||
const {
|
||||
guide,
|
||||
frames,
|
||||
|
|
@ -23,8 +20,7 @@ export default function GuideForm() {
|
|||
handleRemoveStep,
|
||||
handleUpdateStep,
|
||||
handleSaveGuide,
|
||||
isEditing,
|
||||
} = useGuideForm(g);
|
||||
} = useGuideForm(initial);
|
||||
|
||||
return (
|
||||
<div className="new-guide">
|
||||
|
|
@ -46,13 +42,11 @@ export default function GuideForm() {
|
|||
onChange={(e) => handleUpdateDescription(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<FileUpload onChange={handleAddFrames} />
|
||||
|
||||
{frames.length > 0 &&
|
||||
frames.map((frame, index) => (
|
||||
<Frame
|
||||
key={index}
|
||||
key={frame.id}
|
||||
frameIndex={index}
|
||||
frame={frame}
|
||||
onRemove={handleRemoveFrame}
|
||||
|
|
@ -63,13 +57,21 @@ export default function GuideForm() {
|
|||
reOrder={handleReOrderFrames}
|
||||
/>
|
||||
))}
|
||||
|
||||
{frames.length === 0 && <p>No images added yet.</p>}
|
||||
|
||||
{frames.length > 0 && <FileUpload onChange={handleAddFrames} />}
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GuideForm() {
|
||||
const { id } = useParams();
|
||||
const { guides, loading } = useGuides();
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
const initial = id ? guides.find((g) => g.id === id) : undefined;
|
||||
|
||||
return <GuideFormInner initial={initial} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,28 +12,30 @@ import { useIDB } from "./dbinit.context";
|
|||
interface GuideContextValue {
|
||||
guides: CompiledGuideType[];
|
||||
setGuides: React.Dispatch<React.SetStateAction<CompiledGuideType[]>>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const GuideContext = createContext<GuideContextValue | undefined>(undefined);
|
||||
|
||||
export function GuideProvider({ children }: { children: ReactNode }) {
|
||||
const [guides, setGuides] = useState<CompiledGuideType[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { db } = useIDB();
|
||||
|
||||
useEffect(() => {
|
||||
if (!db) return;
|
||||
db.getAll("guides").then((fetchedGuides) => {
|
||||
const sortedGuides = [...fetchedGuides].sort((a, b) => {
|
||||
return (
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
});
|
||||
const sortedGuides = [...fetchedGuides].sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
setGuides(sortedGuides);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [db]);
|
||||
|
||||
return (
|
||||
<GuideContext.Provider value={{ guides, setGuides }}>
|
||||
<GuideContext.Provider value={{ guides, setGuides, loading }}>
|
||||
{children}
|
||||
</GuideContext.Provider>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue