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.
This commit is contained in:
parent
c794d5fd18
commit
3e84310384
10 changed files with 330 additions and 0 deletions
|
|
@ -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<string[][]>([]);
|
||||
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 }) {
|
|||
</p>
|
||||
)}
|
||||
|
||||
{draft.resumable && <LabDraftBanner onResume={draft.resume} onDiscard={draft.discard} />}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Image + Bio */}
|
||||
<Card className="bg-black/60 border-white/10">
|
||||
|
|
|
|||
34
src/components/LabDraftBanner.tsx
Normal file
34
src/components/LabDraftBanner.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { History, X } from 'lucide-react';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
/**
|
||||
* "You have an unsaved draft" banner shown at the top of a Lab profile form when
|
||||
* `useLabDraft` found a saved draft. Resume restores it; Discard throws it away.
|
||||
*/
|
||||
export function LabDraftBanner({ onResume, onDiscard }: { onResume: () => void; onDiscard: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="mb-4 flex items-center gap-3 rounded-xl border border-[#ccff00]/40 bg-[#ccff00]/5 p-3">
|
||||
<History className="h-5 w-5 flex-shrink-0 text-[#ccff00]" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-bold text-white">{t('labUi.draft.title')}</p>
|
||||
<p className="text-xs text-gray-400">{t('labUi.draft.body')}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onResume}
|
||||
className="min-h-0 flex-shrink-0 rounded-lg bg-[#ccff00] px-3 py-1.5 text-xs font-black uppercase text-black hover:bg-[#ccff00]/90"
|
||||
>
|
||||
{t('labUi.draft.resume')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDiscard}
|
||||
aria-label={t('labUi.draft.discard')}
|
||||
className="min-h-0 min-w-0 flex-shrink-0 rounded-full p-1 text-gray-500 hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
src/hooks/useLabDraft.ts
Normal file
62
src/hooks/useLabDraft.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const PREFIX = 'skatespots:lab-draft:';
|
||||
|
||||
/**
|
||||
* Autosave a Lab profile form so a long create/edit isn't lost on nav or reload.
|
||||
*
|
||||
* - Persists `snapshot` to localStorage (content-debounced: the serialized
|
||||
* string only changes when the draft actually changes).
|
||||
* - A saved draft is only offered as **resumable** when it *differs* from the
|
||||
* current form state — so merely opening an existing profile (whose prefill
|
||||
* matches the saved draft) never shows a spurious "resume" banner, and a
|
||||
* draft can never silently clobber a freshly loaded profile.
|
||||
* - Call `clear()` after a successful publish.
|
||||
*/
|
||||
export function useLabDraft<T>(key: string, snapshot: T, apply: (draft: T) => void) {
|
||||
const storageKey = PREFIX + key;
|
||||
const [loaded, setLoaded] = useState<T | null>(null);
|
||||
const firstRun = useRef(true);
|
||||
const serialized = JSON.stringify(snapshot);
|
||||
|
||||
// Load any saved draft once.
|
||||
useEffect(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
if (raw) setLoaded(JSON.parse(raw) as T);
|
||||
} catch {
|
||||
// ignore malformed/blocked storage
|
||||
}
|
||||
}, [storageKey]);
|
||||
|
||||
// Persist on change (skip the initial empty paint).
|
||||
useEffect(() => {
|
||||
if (firstRun.current) {
|
||||
firstRun.current = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(storageKey, serialized);
|
||||
} catch {
|
||||
// storage may be full/blocked — a lost draft is not fatal
|
||||
}
|
||||
}, [serialized, storageKey]);
|
||||
|
||||
const resume = () => {
|
||||
if (!loaded) return;
|
||||
apply(loaded);
|
||||
setLoaded(null);
|
||||
};
|
||||
|
||||
const discard = () => {
|
||||
setLoaded(null);
|
||||
try { localStorage.removeItem(storageKey); } catch { /* ignore */ }
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
try { localStorage.removeItem(storageKey); } catch { /* ignore */ }
|
||||
};
|
||||
|
||||
const resumable = loaded !== null && JSON.stringify(loaded) !== serialized;
|
||||
return { resumable, resume, discard, clear };
|
||||
}
|
||||
|
|
@ -2005,6 +2005,12 @@ labUi: {
|
|||
portfolio: 'Portfolio',
|
||||
note: 'Preview only — nothing is published yet.',
|
||||
},
|
||||
draft: {
|
||||
title: 'You have an unsaved draft',
|
||||
body: 'Pick up where you left off — nothing is published until you press Publish.',
|
||||
resume: 'Resume draft',
|
||||
discard: 'Discard draft',
|
||||
},
|
||||
offerTattoo: 'Offer Tattoo',
|
||||
offerTraining: 'Offer Training / Physio',
|
||||
organizeEvents: 'Organize Events',
|
||||
|
|
|
|||
|
|
@ -2005,6 +2005,12 @@ labUi: {
|
|||
portfolio: 'Portafolio',
|
||||
note: 'Solo vista previa — aún no se ha publicado nada.',
|
||||
},
|
||||
draft: {
|
||||
title: 'Tienes un borrador sin guardar',
|
||||
body: 'Continúa donde lo dejaste — nada se publica hasta que pulses Publicar.',
|
||||
resume: 'Reanudar borrador',
|
||||
discard: 'Descartar borrador',
|
||||
},
|
||||
offerTattoo: 'Ofrecer tatuajes',
|
||||
offerTraining: 'Ofrecer entrenamiento / fisio',
|
||||
organizeEvents: 'Organizar eventos',
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
|||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||
import { LabDraftBanner } from '@/components/LabDraftBanner';
|
||||
import { useLabDraft } from '@/hooks/useLabDraft';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { pickRateUnits } from '@/lib/labExtras';
|
||||
|
||||
|
|
@ -135,12 +137,47 @@ export default function BecomeBuilderPage() {
|
|||
const [prefilled, setPrefilled] = useState(false);
|
||||
const [extraTags, setExtraTags] = useState<string[][]>([]);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
|
||||
const [lead_time, setLeadTime] = useState('');
|
||||
const [warranty, setWarranty] = useState('');
|
||||
const [existingGeohash, setExistingGeohash] = useState<string | undefined>(undefined);
|
||||
|
||||
// Prefill from an existing profile so this is an edit, not a blind overwrite.
|
||||
const { data: existingProfile } = useMyLabProfileEvent(SKATE_BUILDER_KIND, 'builder-profile');
|
||||
const draft = useLabDraft(
|
||||
`craft:builder`,
|
||||
{ titleInput, bio, location, latitude, longitude, selectedServices, selectedSkills, selectedScale, gear, serviceArea, insured, image, experience, rate, phone, email, website, portfolio, social, noffer, entityType, members, crewRef, selectedSpots, lead_time, warranty, extraTags },
|
||||
(d) => {
|
||||
setTitleInput(d.titleInput);
|
||||
setBio(d.bio);
|
||||
setLocation(d.location);
|
||||
setLatitude(d.latitude);
|
||||
setLongitude(d.longitude);
|
||||
setSelectedServices(d.selectedServices);
|
||||
setSelectedSkills(d.selectedSkills);
|
||||
setSelectedScale(d.selectedScale);
|
||||
setGear(d.gear);
|
||||
setServiceArea(d.serviceArea);
|
||||
setInsured(d.insured);
|
||||
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);
|
||||
setSelectedSpots(d.selectedSpots);
|
||||
setLeadTime(d.lead_time);
|
||||
setWarranty(d.warranty);
|
||||
setExtraTags(d.extraTags);
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (prefilled || !existingProfile) return;
|
||||
setPrefilled(true);
|
||||
|
|
@ -325,6 +362,7 @@ export default function BecomeBuilderPage() {
|
|||
content: bio.trim(),
|
||||
tags,
|
||||
});
|
||||
draft.clear();
|
||||
|
||||
toast({ title: existingProfile ? t('labUi.builderForm.updated') : t('labUi.builderForm.created'), description: t('labUi.become.profilePublished') });
|
||||
navigate(`${labProfilePath('builder', user?.pubkey ?? '', identifier)}${existingProfile ? '' : '?published=1'}`);
|
||||
|
|
@ -426,6 +464,8 @@ export default function BecomeBuilderPage() {
|
|||
</Card>
|
||||
</div>
|
||||
|
||||
{draft.resumable && <LabDraftBanner onResume={draft.resume} onDiscard={draft.discard} />}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card className="bg-gray-900 border-gray-700">
|
||||
<CardHeader>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import { cn } from '@/lib/utils';
|
|||
import { GraduationCap, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Check, Eye } from 'lucide-react';
|
||||
import { LabProUpgrade } from '@/components/LabProUpgrade';
|
||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||
import { LabDraftBanner } from '@/components/LabDraftBanner';
|
||||
import { useLabDraft } from '@/hooks/useLabDraft';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { usePlatformFee } from '@/hooks/usePlatformFee';
|
||||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||
|
|
@ -157,6 +159,49 @@ export default function BecomeCoachPage() {
|
|||
const [titleInput, setTitleInput] = useState('');
|
||||
const [prefilled, setPrefilled] = useState(false);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
|
||||
const draft = useLabDraft(
|
||||
`craft:coach`,
|
||||
{ titleInput, bio, location, latitude, longitude, selectedSkills, image, experience, rate, rateAmount, rateCurrency, rateUnit, phone, email, website, instagram, noffer, entityType, members, crewRef, selectedSpots, formats, levels, ageGroups, credentials, certNote, languages, availability, booking, paymentMethods, paymentNote, bookingFee, bookingFeeCurrency, payWhatYouCan, photos, video },
|
||||
(d) => {
|
||||
setTitleInput(d.titleInput);
|
||||
setBio(d.bio);
|
||||
setLocation(d.location);
|
||||
setLatitude(d.latitude);
|
||||
setLongitude(d.longitude);
|
||||
setSelectedSkills(d.selectedSkills);
|
||||
setImage(d.image);
|
||||
setExperience(d.experience);
|
||||
setRate(d.rate);
|
||||
setRateAmount(d.rateAmount);
|
||||
setRateCurrency(d.rateCurrency);
|
||||
setRateUnit(d.rateUnit);
|
||||
setPhone(d.phone);
|
||||
setEmail(d.email);
|
||||
setWebsite(d.website);
|
||||
setInstagram(d.instagram);
|
||||
setNoffer(d.noffer);
|
||||
setEntityType(d.entityType);
|
||||
setMembers(d.members);
|
||||
setCrewRef(d.crewRef);
|
||||
setSelectedSpots(d.selectedSpots);
|
||||
setFormats(d.formats);
|
||||
setLevels(d.levels);
|
||||
setAgeGroups(d.ageGroups);
|
||||
setCredentials(d.credentials);
|
||||
setCertNote(d.certNote);
|
||||
setLanguages(d.languages);
|
||||
setAvailability(d.availability);
|
||||
setBooking(d.booking);
|
||||
setPaymentMethods(d.paymentMethods);
|
||||
setPaymentNote(d.paymentNote);
|
||||
setBookingFee(d.bookingFee);
|
||||
setBookingFeeCurrency(d.bookingFeeCurrency);
|
||||
setPayWhatYouCan(d.payWhatYouCan);
|
||||
setPhotos(d.photos);
|
||||
setVideo(d.video);
|
||||
},
|
||||
);
|
||||
const [existingGeohash, setExistingGeohash] = useState<string | undefined>(undefined);
|
||||
|
||||
// Prefill from an existing profile so this is an edit, not a blind overwrite.
|
||||
|
|
@ -412,6 +457,7 @@ export default function BecomeCoachPage() {
|
|||
content: bio.trim(),
|
||||
tags,
|
||||
});
|
||||
draft.clear();
|
||||
|
||||
toast({ title: existingProfile ? t('labUi.coachForm.updated') : t('labUi.coachForm.created'), description: t('labUi.become.profilePublished') });
|
||||
navigate(`${labProfilePath('coach', user?.pubkey ?? '', identifier)}${existingProfile ? '' : '?published=1'}`);
|
||||
|
|
@ -523,6 +569,8 @@ export default function BecomeCoachPage() {
|
|||
</Card>
|
||||
</div>
|
||||
|
||||
{draft.resumable && <LabDraftBanner onResume={draft.resume} onDiscard={draft.discard} />}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card className="bg-gray-900 border-gray-700">
|
||||
<CardHeader>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
|||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||
import { LabDraftBanner } from '@/components/LabDraftBanner';
|
||||
import { useLabDraft } from '@/hooks/useLabDraft';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { pickRateUnits } from '@/lib/labExtras';
|
||||
|
||||
|
|
@ -91,12 +93,44 @@ export default function BecomeEditorPage() {
|
|||
const [prefilled, setPrefilled] = useState(false);
|
||||
const [extraTags, setExtraTags] = useState<string[][]>([]);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
|
||||
const [turnaround, setTurnaround] = useState('');
|
||||
const [deliverables, setDeliverables] = useState('');
|
||||
const [existingGeohash, setExistingGeohash] = useState<string | undefined>(undefined);
|
||||
|
||||
// Prefill from an existing profile so this is an edit, not a blind overwrite.
|
||||
const { data: existingProfile } = useMyLabProfileEvent(SKATE_EDITOR_KIND, 'editor-profile');
|
||||
const draft = useLabDraft(
|
||||
`craft:editor`,
|
||||
{ titleInput, bio, location, latitude, longitude, selectedServices, selectedSkills, image, experience, rate, phone, email, website, portfolio, social, noffer, entityType, members, crewRef, hardware, software, turnaround, deliverables, extraTags },
|
||||
(d) => {
|
||||
setTitleInput(d.titleInput);
|
||||
setBio(d.bio);
|
||||
setLocation(d.location);
|
||||
setLatitude(d.latitude);
|
||||
setLongitude(d.longitude);
|
||||
setSelectedServices(d.selectedServices);
|
||||
setSelectedSkills(d.selectedSkills);
|
||||
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);
|
||||
setHardware(d.hardware);
|
||||
setSoftware(d.software);
|
||||
setTurnaround(d.turnaround);
|
||||
setDeliverables(d.deliverables);
|
||||
setExtraTags(d.extraTags);
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (prefilled || !existingProfile) return;
|
||||
setPrefilled(true);
|
||||
|
|
@ -250,6 +284,7 @@ export default function BecomeEditorPage() {
|
|||
}
|
||||
|
||||
await createEvent({ kind: SKATE_EDITOR_KIND, content: bio.trim(), tags });
|
||||
draft.clear();
|
||||
|
||||
toast({ title: existingProfile ? t('labUi.editorForm.updated') : t('labUi.editorForm.created'), description: t('labUi.editorForm.published') });
|
||||
navigate(`${labProfilePath('editor', user?.pubkey ?? '', identifier)}${existingProfile ? '' : '?published=1'}`);
|
||||
|
|
@ -344,6 +379,8 @@ export default function BecomeEditorPage() {
|
|||
</Card>
|
||||
</div>
|
||||
|
||||
{draft.resumable && <LabDraftBanner onResume={draft.resume} onDiscard={draft.discard} />}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card className="bg-gray-900 border-gray-700">
|
||||
<CardHeader>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
|||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||
import { LabDraftBanner } from '@/components/LabDraftBanner';
|
||||
import { useLabDraft } from '@/hooks/useLabDraft';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { pickRateUnits } from '@/lib/labExtras';
|
||||
|
||||
|
|
@ -86,6 +88,7 @@ export default function BecomeFilmerPage() {
|
|||
const [prefilled, setPrefilled] = useState(false);
|
||||
const [extraTags, setExtraTags] = useState<string[][]>([]);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
|
||||
const [turnaround, setTurnaround] = useState('');
|
||||
const [deliverables, setDeliverables] = useState('');
|
||||
const [drone, setDrone] = useState('');
|
||||
|
|
@ -93,6 +96,38 @@ export default function BecomeFilmerPage() {
|
|||
|
||||
// Prefill from an existing profile so this is an edit, not a blind overwrite.
|
||||
const { data: existingProfile } = useMyLabProfileEvent(SKATE_FILMER_KIND, 'filming-profile');
|
||||
const draft = useLabDraft(
|
||||
`craft:filmer`,
|
||||
{ titleInput, bio, location, latitude, longitude, selectedServices, selectedSkills, image, experience, rate, phone, email, website, portfolio, social, noffer, entityType, members, crewRef, gear, selectedSpots, turnaround, deliverables, drone, extraTags },
|
||||
(d) => {
|
||||
setTitleInput(d.titleInput);
|
||||
setBio(d.bio);
|
||||
setLocation(d.location);
|
||||
setLatitude(d.latitude);
|
||||
setLongitude(d.longitude);
|
||||
setSelectedServices(d.selectedServices);
|
||||
setSelectedSkills(d.selectedSkills);
|
||||
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);
|
||||
setGear(d.gear);
|
||||
setSelectedSpots(d.selectedSpots);
|
||||
setTurnaround(d.turnaround);
|
||||
setDeliverables(d.deliverables);
|
||||
setDrone(d.drone);
|
||||
setExtraTags(d.extraTags);
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (prefilled || !existingProfile) return;
|
||||
setPrefilled(true);
|
||||
|
|
@ -282,6 +317,7 @@ export default function BecomeFilmerPage() {
|
|||
content: bio.trim(),
|
||||
tags,
|
||||
});
|
||||
draft.clear();
|
||||
|
||||
toast({ title: existingProfile ? t('labUi.filmerForm.updated') : t('labUi.filmerForm.created'), description: t('labUi.become.profilePublished') });
|
||||
navigate(`${labProfilePath('filmer', user?.pubkey ?? '', identifier)}${existingProfile ? '' : '?published=1'}`);
|
||||
|
|
@ -393,6 +429,8 @@ export default function BecomeFilmerPage() {
|
|||
</Card>
|
||||
</div>
|
||||
|
||||
{draft.resumable && <LabDraftBanner onResume={draft.resume} onDiscard={draft.discard} />}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card className="bg-gray-900 border-gray-700">
|
||||
<CardHeader>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ import { usePlatformFee } from '@/hooks/usePlatformFee';
|
|||
import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile';
|
||||
import { LabExtrasFields } from '@/components/LabExtrasFields';
|
||||
import { LabProfilePreview } from '@/components/LabProfilePreview';
|
||||
import { LabDraftBanner } from '@/components/LabDraftBanner';
|
||||
import { useLabDraft } from '@/hooks/useLabDraft';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { pickRateUnits } from '@/lib/labExtras';
|
||||
|
||||
|
|
@ -145,12 +147,47 @@ export default function BecomePhotographerPage() {
|
|||
const [prefilled, setPrefilled] = useState(false);
|
||||
const [extraTags, setExtraTags] = useState<string[][]>([]);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
|
||||
const [turnaround, setTurnaround] = useState('');
|
||||
const [deliverables, setDeliverables] = useState('');
|
||||
const [existingGeohash, setExistingGeohash] = useState<string | undefined>(undefined);
|
||||
|
||||
// Prefill from an existing profile so this is an edit, not a blind overwrite.
|
||||
const { data: existingProfile } = useMyLabProfileEvent(SKATE_PHOTOGRAPHER_KIND, 'photography-profile');
|
||||
const draft = useLabDraft(
|
||||
`craft:photographer`,
|
||||
{ titleInput, bio, location, latitude, longitude, selectedServices, selectedCameras, selectedLenses, selectedLighting, selectedSupport, selectedStyles, gear, image, experience, rate, phone, email, website, portfolio, social, noffer, entityType, members, crewRef, turnaround, deliverables, extraTags },
|
||||
(d) => {
|
||||
setTitleInput(d.titleInput);
|
||||
setBio(d.bio);
|
||||
setLocation(d.location);
|
||||
setLatitude(d.latitude);
|
||||
setLongitude(d.longitude);
|
||||
setSelectedServices(d.selectedServices);
|
||||
setSelectedCameras(d.selectedCameras);
|
||||
setSelectedLenses(d.selectedLenses);
|
||||
setSelectedLighting(d.selectedLighting);
|
||||
setSelectedSupport(d.selectedSupport);
|
||||
setSelectedStyles(d.selectedStyles);
|
||||
setGear(d.gear);
|
||||
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);
|
||||
setTurnaround(d.turnaround);
|
||||
setDeliverables(d.deliverables);
|
||||
setExtraTags(d.extraTags);
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (prefilled || !existingProfile) return;
|
||||
setPrefilled(true);
|
||||
|
|
@ -349,6 +386,7 @@ export default function BecomePhotographerPage() {
|
|||
content: bio.trim(),
|
||||
tags,
|
||||
});
|
||||
draft.clear();
|
||||
|
||||
toast({ title: existingProfile ? t('labUi.photoForm.updated') : t('labUi.photoForm.created'), description: t('labUi.become.profilePublished') });
|
||||
navigate(`${labProfilePath('photographer', user?.pubkey ?? '', identifier)}${existingProfile ? '' : '?published=1'}`);
|
||||
|
|
@ -450,6 +488,8 @@ export default function BecomePhotographerPage() {
|
|||
</Card>
|
||||
</div>
|
||||
|
||||
{draft.resumable && <LabDraftBanner onResume={draft.resume} onDiscard={draft.discard} />}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card className="bg-gray-900 border-gray-700">
|
||||
<CardHeader>
|
||||
|
|
|
|||
Loading…
Reference in a new issue