From 3e84310384c2aeb6bd2c318884d66907756f4d71 Mon Sep 17 00:00:00 2001
From: SkateSpots
Date: Sun, 4 Oct 2026 11:35:41 +0700
Subject: [PATCH] Autosave Lab profile drafts so long forms survive nav/reload
- useLabDraft persists the form snapshot to localStorage (content-debounced)
and only offers a saved draft as resumable when it differs from the current
state, so opening an existing profile never shows a spurious resume banner and
a draft can't silently clobber a freshly loaded profile.
- LabDraftBanner offers Resume / Discard; wired into CraftProfileForm (8 crafts)
and the coach + filmer/editor/photographer/builder pages.
- The draft clears on a successful publish.
---
src/components/CraftProfileForm.tsx | 19 +++++++++
src/components/LabDraftBanner.tsx | 34 +++++++++++++++
src/hooks/useLabDraft.ts | 62 ++++++++++++++++++++++++++++
src/i18n/en.ts | 6 +++
src/i18n/es.ts | 6 +++
src/pages/BecomeBuilderPage.tsx | 40 ++++++++++++++++++
src/pages/BecomeCoachPage.tsx | 48 +++++++++++++++++++++
src/pages/BecomeEditorPage.tsx | 37 +++++++++++++++++
src/pages/BecomeFilmerPage.tsx | 38 +++++++++++++++++
src/pages/BecomePhotographerPage.tsx | 40 ++++++++++++++++++
10 files changed, 330 insertions(+)
create mode 100644 src/components/LabDraftBanner.tsx
create mode 100644 src/hooks/useLabDraft.ts
diff --git a/src/components/CraftProfileForm.tsx b/src/components/CraftProfileForm.tsx
index 57704e2..a2ef469 100644
--- a/src/components/CraftProfileForm.tsx
+++ b/src/components/CraftProfileForm.tsx
@@ -23,6 +23,8 @@ import { CrewLinkField } from '@/components/CrewLinkField';
import { LabProUpgrade } from '@/components/LabProUpgrade';
import { usePlatformFee } from '@/hooks/usePlatformFee';
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
+import { useLabDraft } from '@/hooks/useLabDraft';
+import { LabDraftBanner } from '@/components/LabDraftBanner';
import { isValidNoffer, type EntityType } from '@/lib/types';
import type { LabOption } from '@/lib/labExtras';
import { labProfilePathFromKind } from '@/lib/labRoutes';
@@ -137,6 +139,20 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
const [extraTags, setExtraTags] = useState([]);
const [previewOpen, setPreviewOpen] = useState(false);
+ const draft = useLabDraft(
+ `craft:${kind}:${identifier}`,
+ { titleInput, bio, location, latitude, longitude, selected, image, experience, rate, phone, email, website, portfolio, social, noffer, entityType, members, crewRef, extras, extraTags },
+ (d) => {
+ setTitleInput(d.titleInput); setBio(d.bio); setLocation(d.location);
+ setLatitude(d.latitude); setLongitude(d.longitude); setSelected(d.selected);
+ setImage(d.image); setExperience(d.experience); setRate(d.rate);
+ setPhone(d.phone); setEmail(d.email); setWebsite(d.website);
+ setPortfolio(d.portfolio); setSocial(d.social); setNoffer(d.noffer);
+ setEntityType(d.entityType); setMembers(d.members); setCrewRef(d.crewRef);
+ setExtras(d.extras); setExtraTags(d.extraTags);
+ },
+ );
+
// Load + prefill an existing profile so this is an EDIT, not a blind overwrite.
const { data: existingProfile } = useMyLabProfileEvent(kind, identifier);
useEffect(() => {
@@ -310,6 +326,7 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
if (isPro && goPro && featuredUntilSeconds) tags.push(['featured', String(featuredUntilSeconds)]);
await createEvent({ kind, content: bio.trim(), tags });
+ draft.clear();
toast({ title: t(existingProfile ? 'labUi.craftForm.updated' : 'labUi.craftForm.created'), description: t('labUi.become.profilePublished') });
navigate(`${labProfilePathFromKind(kind, user?.pubkey ?? '', identifier)}${existingProfile ? '' : '?published=1'}`);
} catch (error) {
@@ -392,6 +409,8 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
)}
+ {draft.resumable && }
+