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 "./Frame.css";
|
||||||
import type { FrameType, StepType } from "../../../lib/schemas";
|
import type { FrameType, StepType } from "../../../lib/schemas";
|
||||||
import Step from "./Step";
|
import Step from "./Step";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
export default function Frame({
|
export default function Frame({
|
||||||
frame,
|
frame,
|
||||||
|
|
@ -21,6 +22,15 @@ export default function Frame({
|
||||||
updateTitle: (frameId: string, title: string) => void;
|
updateTitle: (frameId: string, title: string) => void;
|
||||||
reOrder: (startIndex: number, endIndex: number) => void;
|
reOrder: (startIndex: number, endIndex: number) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const objectUrl = useMemo(
|
||||||
|
() => URL.createObjectURL(frame.file),
|
||||||
|
[frame.file],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => URL.revokeObjectURL(objectUrl);
|
||||||
|
}, [objectUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="frame">
|
<div className="frame">
|
||||||
<div className="buttons">
|
<div className="buttons">
|
||||||
|
|
@ -38,7 +48,6 @@ export default function Frame({
|
||||||
>
|
>
|
||||||
Down
|
Down
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="remove"
|
className="remove"
|
||||||
|
|
@ -47,7 +56,6 @@ export default function Frame({
|
||||||
Remove Frame
|
Remove Frame
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Title
|
Title
|
||||||
<input
|
<input
|
||||||
|
|
@ -56,7 +64,7 @@ export default function Frame({
|
||||||
onChange={(e) => updateTitle(frame.id, e.target.value)}
|
onChange={(e) => updateTitle(frame.id, e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<img src={URL.createObjectURL(frame.file)} alt={frame.title} />
|
<img src={objectUrl} alt={frame.title} />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="add-step"
|
className="add-step"
|
||||||
|
|
@ -64,7 +72,6 @@ export default function Frame({
|
||||||
>
|
>
|
||||||
Add Step
|
Add Step
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="steps">
|
<div className="steps">
|
||||||
{frame.steps.map((step: StepType, index: number) => (
|
{frame.steps.map((step: StepType, index: number) => (
|
||||||
<Step
|
<Step
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
type CompiledGuideType,
|
type CompiledGuideType,
|
||||||
type FrameType,
|
type FrameType,
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,9 @@ import useGuideForm from "../hooks/useGuideForm";
|
||||||
import Frame from "../components/Frame";
|
import Frame from "../components/Frame";
|
||||||
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 type { CompiledGuideType } from "../../../lib/schemas";
|
||||||
|
|
||||||
export default function GuideForm() {
|
function GuideFormInner({ initial }: { initial?: CompiledGuideType }) {
|
||||||
const { id } = useParams();
|
|
||||||
const { guides } = useGuides();
|
|
||||||
const g = guides.find((g) => g.id === id);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
guide,
|
guide,
|
||||||
frames,
|
frames,
|
||||||
|
|
@ -23,8 +20,7 @@ export default function GuideForm() {
|
||||||
handleRemoveStep,
|
handleRemoveStep,
|
||||||
handleUpdateStep,
|
handleUpdateStep,
|
||||||
handleSaveGuide,
|
handleSaveGuide,
|
||||||
isEditing,
|
} = useGuideForm(initial);
|
||||||
} = useGuideForm(g);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="new-guide">
|
<div className="new-guide">
|
||||||
|
|
@ -46,13 +42,11 @@ export default function GuideForm() {
|
||||||
onChange={(e) => handleUpdateDescription(e.target.value)}
|
onChange={(e) => handleUpdateDescription(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<FileUpload onChange={handleAddFrames} />
|
<FileUpload onChange={handleAddFrames} />
|
||||||
|
|
||||||
{frames.length > 0 &&
|
{frames.length > 0 &&
|
||||||
frames.map((frame, index) => (
|
frames.map((frame, index) => (
|
||||||
<Frame
|
<Frame
|
||||||
key={index}
|
key={frame.id}
|
||||||
frameIndex={index}
|
frameIndex={index}
|
||||||
frame={frame}
|
frame={frame}
|
||||||
onRemove={handleRemoveFrame}
|
onRemove={handleRemoveFrame}
|
||||||
|
|
@ -63,13 +57,21 @@ export default function GuideForm() {
|
||||||
reOrder={handleReOrderFrames}
|
reOrder={handleReOrderFrames}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{frames.length === 0 && <p>No images added yet.</p>}
|
{frames.length === 0 && <p>No images added yet.</p>}
|
||||||
|
|
||||||
{frames.length > 0 && <FileUpload onChange={handleAddFrames} />}
|
{frames.length > 0 && <FileUpload onChange={handleAddFrames} />}
|
||||||
|
|
||||||
<button type="submit">Save</button>
|
<button type="submit">Save</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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 {
|
interface GuideContextValue {
|
||||||
guides: CompiledGuideType[];
|
guides: CompiledGuideType[];
|
||||||
setGuides: React.Dispatch<React.SetStateAction<CompiledGuideType[]>>;
|
setGuides: React.Dispatch<React.SetStateAction<CompiledGuideType[]>>;
|
||||||
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GuideContext = createContext<GuideContextValue | undefined>(undefined);
|
const GuideContext = createContext<GuideContextValue | undefined>(undefined);
|
||||||
|
|
||||||
export function GuideProvider({ children }: { children: ReactNode }) {
|
export function GuideProvider({ children }: { children: ReactNode }) {
|
||||||
const [guides, setGuides] = useState<CompiledGuideType[]>([]);
|
const [guides, setGuides] = useState<CompiledGuideType[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const { db } = useIDB();
|
const { db } = useIDB();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!db) return;
|
if (!db) return;
|
||||||
db.getAll("guides").then((fetchedGuides) => {
|
db.getAll("guides").then((fetchedGuides) => {
|
||||||
const sortedGuides = [...fetchedGuides].sort((a, b) => {
|
const sortedGuides = [...fetchedGuides].sort(
|
||||||
return (
|
(a, b) =>
|
||||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
setGuides(sortedGuides);
|
setGuides(sortedGuides);
|
||||||
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}, [db]);
|
}, [db]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GuideContext.Provider value={{ guides, setGuides }}>
|
<GuideContext.Provider value={{ guides, setGuides, loading }}>
|
||||||
{children}
|
{children}
|
||||||
</GuideContext.Provider>
|
</GuideContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue