Add craft-specific detail fields with matching display sections

Per-craft freeform fields, each wired end-to-end (form + detail display) so
none are stored invisibly:

- Artist: deliverables, turnaround
- Graffiti: surfaces, max_size
- Musician: set_length, tech_rider
- Judge: travel, certifications
- Organizer: insurance, permits
- Tattoo: deposit, minimum
- Trainer: modalities
- Barber: duration
- Filmer/Editor/Photographer: turnaround, deliverables (filmer adds drone)
- Builder: lead_time, warranty

Data-driven crafts declare them via CraftProfileForm's extraFields (now with
labelKey); the bespoke pages add matching state/inputs/prefill/save.
src/lib/craftFields.ts (CRAFT_DETAIL_FIELDS) maps each tag to a display label,
rendered on the shared detail page; toggle tags render as a localized Yes.
Labels are translated (EN/ES); placeholders stay as examples.
This commit is contained in:
SkateSpots 2026-10-04 10:53:51 +07:00
parent 88a3e4d1b4
commit 681eab9592
12 changed files with 251 additions and 3 deletions

View file

@ -112,7 +112,10 @@ session with **no other memory of this project**. So:
`skillSections` + `extraFields`, and per-craft `rateUnits` (billing units) are
set in `becomeRoles.tsx`. `/lab` filters read `parseLabExtras` for all crafts.
⚠️ A new craft-specific freeform `extraField` is **invisible** unless the
detail page has a matching `pget('…')` section — add both together.
detail page has a matching section — declare it in **`src/lib/craftFields.ts`**
(`CRAFT_DETAIL_FIELDS`) as well as the form. A small per-craft set is in place
(deliverables/turnaround, deposit/minimum, travel, lead_time, …); toggle tags
store `"true"` and render as a localized "Yes".
- **Photos**: `PhotoDropzone` (`src/components/PhotoDropzone.tsx`) is the shared
image input — **drag & drop + multi-select**, emits `File[]` to its caller
(which owns uploads/toasts). Used by `CreateSpotPage`, `EditSpotPage` and

17
NIP.md
View file

@ -1912,6 +1912,23 @@ block (`LabExtrasSection`) renders for every craft:
> are deliberately out of scope. No tag named `payment` — that's the marketplace
> offer pointer.
**Craft-specific detail fields.** Each craft may add a couple of freeform tags
for context; they're stored verbatim and rendered by `src/lib/craftFields.ts`
(so a field with no entry there is invisible):
- **Artist**: `deliverables`, `turnaround`
- **Graffiti**: `surfaces`, `max_size`
- **Musician**: `set_length`, `tech_rider`
- **Judge**: `travel`, `certifications`
- **Organizer**: `insurance`, `permits`
- **Tattoo**: `deposit`, `minimum`
- **Trainer**: `modalities`
- **Barber**: `duration`
- **Filmer**: `turnaround`, `deliverables`, `drone`
- **Editor**: `turnaround`, `deliverables`
- **Photographer**: `turnaround`, `deliverables`
- **Builder**: `lead_time`, `warranty`
### Pro / Featured Listings (`featured` tag)
Lab profiles, calendar events (kind 31923), and skate tours (kind 30823)

View file

@ -45,6 +45,8 @@ export interface ExtraField {
/** Nostr tag name to write, e.g. "studio", "spotify". */
tag: string;
label: string;
/** Optional i18n key for the label (takes precedence over `label`). */
labelKey?: string;
placeholder?: string;
/** "text" (default) | "url" | "toggle" */
type?: 'text' | 'url' | 'toggle';
@ -479,7 +481,7 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
f.type === 'toggle' ? (
<div key={f.tag} className="flex items-center justify-between gap-3">
<div>
<Label className="text-sm font-bold text-gray-200">{f.label}</Label>
<Label className="text-sm font-bold text-gray-200">{f.labelKey ? t(f.labelKey) : f.label}</Label>
{f.helper && <p className="text-xs text-gray-500">{f.helper}</p>}
</div>
<Switch
@ -489,7 +491,7 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
</div>
) : (
<div key={f.tag}>
<Label htmlFor={f.tag} className="text-xs font-bold uppercase tracking-widest text-gray-400">{f.label}</Label>
<Label htmlFor={f.tag} className="text-xs font-bold uppercase tracking-widest text-gray-400">{f.labelKey ? t(f.labelKey) : f.label}</Label>
<Input
id={f.tag}
value={extras[f.tag] ?? ''}

View file

@ -2713,6 +2713,26 @@ labUi: {
previewValidThru: 'Valid thru',
previewNostrVerified: 'Nostr-verified',
},
craftFields: {
detailsTitle: 'Additional details',
deliverables: 'Deliverables',
turnaround: 'Turnaround',
surfaces: 'Surfaces / Walls',
maxSize: 'Max Size',
setLength: 'Set Length',
techRider: 'Tech Rider',
travel: 'Travel',
certifications: 'Certifications',
insurance: 'Insurance',
permits: 'Permits',
deposit: 'Deposit',
minimum: 'Minimum',
modalities: 'Modalities',
duration: 'Appointment Length',
drone: 'Drone',
leadTime: 'Lead Time',
warranty: 'Warranty',
},
craftForm: {
profileImage: 'Profile image',
nameHeadline: 'Name / headline',

View file

@ -2713,6 +2713,26 @@ labUi: {
previewValidThru: 'Válido hasta',
previewNostrVerified: 'Verificado con Nostr',
},
craftFields: {
detailsTitle: 'Detalles adicionales',
deliverables: 'Entregables',
turnaround: 'Plazo',
surfaces: 'Superficies / Muros',
maxSize: 'Tamaño máximo',
setLength: 'Duración del set',
techRider: 'Rider técnico',
travel: 'Desplazamiento',
certifications: 'Certificaciones',
insurance: 'Seguro',
permits: 'Permisos',
deposit: 'Señal',
minimum: 'Mínimo',
modalities: 'Modalidades',
duration: 'Duración de la cita',
drone: 'Dron',
leadTime: 'Plazo de entrega',
warranty: 'Garantía',
},
craftForm: {
profileImage: 'Imagen de perfil',
nameHeadline: 'Nombre / titular',

View file

@ -83,6 +83,10 @@ export const BECOME_ROLES: Record<string, BecomeRole> = {
accentChipHover: 'hover:border-fuchsia-500/60 hover:text-fuchsia-400',
icon: Palette,
rateUnits: pickRateUnits('piece', 'project', 'hour', 'day'),
extraFields: [
{ tag: 'deliverables', label: 'Deliverables', labelKey: 'labUi.craftFields.deliverables', placeholder: 'e.g. print-ready files, source files, deck graphics' },
{ tag: 'turnaround', label: 'Turnaround', labelKey: 'labUi.craftFields.turnaround', placeholder: 'e.g. 3–5 days' },
],
rateLabel: 'Rate',
portfolioLabel: 'Portfolio / Shop link',
skillSections: [
@ -118,6 +122,8 @@ export const BECOME_ROLES: Record<string, BecomeRole> = {
],
extraFields: [
{ tag: 'crew', label: 'Graffiti Crew', placeholder: 'Your writing crew (optional)', helper: 'Separate from your skate crew — the writers you paint with.' },
{ tag: 'surfaces', label: 'Surfaces / Walls', labelKey: 'labUi.craftFields.surfaces', placeholder: 'e.g. concrete, brick, permitted walls' },
{ tag: 'max_size', label: 'Max Size', labelKey: 'labUi.craftFields.maxSize', placeholder: 'e.g. up to 10 m' },
],
},
},
@ -150,6 +156,8 @@ export const BECOME_ROLES: Record<string, BecomeRole> = {
{ tag: 'bandcamp', label: 'Bandcamp', type: 'url', placeholder: 'https://…bandcamp.com' },
{ tag: 'soundcloud', label: 'SoundCloud', type: 'url', placeholder: 'https://soundcloud.com/…' },
{ tag: 'youtube', label: 'YouTube', type: 'url', placeholder: 'https://youtube.com/…' },
{ tag: 'set_length', label: 'Set Length', labelKey: 'labUi.craftFields.setLength', placeholder: 'e.g. 30–45 min' },
{ tag: 'tech_rider', label: 'Tech Rider', labelKey: 'labUi.craftFields.techRider', placeholder: 'e.g. PA, mics, backline' },
],
},
},
@ -179,6 +187,8 @@ export const BECOME_ROLES: Record<string, BecomeRole> = {
],
extraFields: [
{ tag: 'events_judged', label: 'Events Judged', placeholder: 'e.g. 40+ contests, 3 World Tour stops', helper: 'Notable events, comps or tours you have officiated.' },
{ tag: 'travel', label: 'Travel', labelKey: 'labUi.craftFields.travel', placeholder: 'e.g. regional / nationwide / international' },
{ tag: 'certifications', label: 'Certifications', labelKey: 'labUi.craftFields.certifications', placeholder: 'e.g. national judging course' },
],
},
},
@ -208,6 +218,8 @@ export const BECOME_ROLES: Record<string, BecomeRole> = {
],
extraFields: [
{ tag: 'events_hosted', label: 'Events Organized', placeholder: 'e.g. 25 jams, 3 city comps', helper: 'Notable events, comps or meetups you have run.' },
{ tag: 'insurance', label: 'Insurance', labelKey: 'labUi.craftFields.insurance', placeholder: 'e.g. public liability included' },
{ tag: 'permits', label: 'Permits', labelKey: 'labUi.craftFields.permits', placeholder: 'e.g. council permits handled' },
],
},
},
@ -237,6 +249,8 @@ export const BECOME_ROLES: Record<string, BecomeRole> = {
extraFields: [
{ tag: 'studio', label: 'Studio / Shop', placeholder: 'Studio name (optional)' },
{ tag: 'books_open', label: 'Books Open', type: 'toggle', helper: 'Are you currently taking bookings?' },
{ tag: 'deposit', label: 'Deposit', labelKey: 'labUi.craftFields.deposit', placeholder: 'e.g. 50 EUR non-refundable' },
{ tag: 'minimum', label: 'Minimum', labelKey: 'labUi.craftFields.minimum', placeholder: 'e.g. 80 EUR' },
],
},
},
@ -267,6 +281,7 @@ export const BECOME_ROLES: Record<string, BecomeRole> = {
extraFields: [
{ tag: 'credentials', label: 'Certifications', placeholder: 'e.g. BSc Physiotherapy, CSCS…', helper: 'List real qualifications — skaters trust verifiable credentials.' },
{ tag: 'remote', label: 'Remote / Online', type: 'toggle', helper: 'Do you offer online coaching or consultations?' },
{ tag: 'modalities', label: 'Modalities', labelKey: 'labUi.craftFields.modalities', placeholder: 'e.g. hands-on, S&C, dry needling' },
],
},
},
@ -297,6 +312,7 @@ export const BECOME_ROLES: Record<string, BecomeRole> = {
extraFields: [
{ tag: 'studio', label: 'Shop / Studio', placeholder: 'Shop or studio name (optional)' },
{ tag: 'books_open', label: 'Books Open', type: 'toggle', helper: 'Are you currently taking bookings?' },
{ tag: 'duration', label: 'Appointment Length', labelKey: 'labUi.craftFields.duration', placeholder: 'e.g. 30–45 min' },
],
},
},

64
src/lib/craftFields.ts Normal file
View file

@ -0,0 +1,64 @@
import type { LabProfileType } from '@/hooks/useLabProfiles';
/** One craft-specific freeform field shown on the Lab detail page. */
export interface CraftDetailField {
tag: string;
labelKey: string;
}
/**
* Craft-specific freeform fields to surface on a provider's detail page.
*
* Keep this in sync with the `extraFields` declared in `becomeRoles.tsx` (and
* the bespoke become pages): a field with no entry here is stored but invisible.
* Toggle tags (value `"true"`) render as a localized "Yes".
*/
export const CRAFT_DETAIL_FIELDS: Partial<Record<LabProfileType, CraftDetailField[]>> = {
artist: [
{ tag: 'deliverables', labelKey: 'labUi.craftFields.deliverables' },
{ tag: 'turnaround', labelKey: 'labUi.craftFields.turnaround' },
],
graffiti: [
{ tag: 'surfaces', labelKey: 'labUi.craftFields.surfaces' },
{ tag: 'max_size', labelKey: 'labUi.craftFields.maxSize' },
],
musician: [
{ tag: 'set_length', labelKey: 'labUi.craftFields.setLength' },
{ tag: 'tech_rider', labelKey: 'labUi.craftFields.techRider' },
],
judge: [
{ tag: 'travel', labelKey: 'labUi.craftFields.travel' },
{ tag: 'certifications', labelKey: 'labUi.craftFields.certifications' },
],
organizer: [
{ tag: 'insurance', labelKey: 'labUi.craftFields.insurance' },
{ tag: 'permits', labelKey: 'labUi.craftFields.permits' },
],
tattoo: [
{ tag: 'deposit', labelKey: 'labUi.craftFields.deposit' },
{ tag: 'minimum', labelKey: 'labUi.craftFields.minimum' },
],
trainer: [
{ tag: 'modalities', labelKey: 'labUi.craftFields.modalities' },
],
barber: [
{ tag: 'duration', labelKey: 'labUi.craftFields.duration' },
],
filmer: [
{ tag: 'turnaround', labelKey: 'labUi.craftFields.turnaround' },
{ tag: 'deliverables', labelKey: 'labUi.craftFields.deliverables' },
{ tag: 'drone', labelKey: 'labUi.craftFields.drone' },
],
editor: [
{ tag: 'turnaround', labelKey: 'labUi.craftFields.turnaround' },
{ tag: 'deliverables', labelKey: 'labUi.craftFields.deliverables' },
],
photographer: [
{ tag: 'turnaround', labelKey: 'labUi.craftFields.turnaround' },
{ tag: 'deliverables', labelKey: 'labUi.craftFields.deliverables' },
],
builder: [
{ tag: 'lead_time', labelKey: 'labUi.craftFields.leadTime' },
{ tag: 'warranty', labelKey: 'labUi.craftFields.warranty' },
],
};

View file

@ -132,6 +132,8 @@ export default function BecomeBuilderPage() {
const [titleInput, setTitleInput] = useState('');
const [prefilled, setPrefilled] = useState(false);
const [extraTags, setExtraTags] = useState<string[][]>([]);
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.
@ -154,6 +156,8 @@ export default function BecomeBuilderPage() {
setImage(tag('image') ?? null);
setExperience(tag('experience') ?? '');
setRate(tag('rate') ?? '');
setLeadTime(tag('lead_time') ?? '');
setWarranty(tag('warranty') ?? '');
setPhone(tag('phone') ?? '');
setEmail(tag('email') ?? '');
setWebsite(tag('website') ?? '');
@ -296,6 +300,8 @@ export default function BecomeBuilderPage() {
tags.push(['entity', entityType]);
for (const extra of extraTags) tags.push(extra);
if (lead_time.trim()) tags.push(['lead_time', lead_time.trim()]);
if (warranty.trim()) tags.push(['warranty', warranty.trim()]);
if (entityType === 'collective') {
for (const pk of members) tags.push(['p', pk]);
if (crewRef) tags.push(['a', crewRef]);
@ -621,6 +627,20 @@ export default function BecomeBuilderPage() {
</div>
{/* Optional Pro upgrade — free to list without it */}
<Card className="bg-gray-900 border-gray-700">
<CardContent className="p-4 space-y-4">
<Label className="text-base font-bold">{t('labUi.craftFields.detailsTitle')}</Label>
<div className="space-y-2">
<Label>{t('labUi.craftFields.leadTime')}</Label>
<Input value={lead_time} onChange={(e) => setLeadTime(e.target.value)} placeholder="e.g. 2–4 weeks" className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label>{t('labUi.craftFields.warranty')}</Label>
<Input value={warranty} onChange={(e) => setWarranty(e.target.value)} placeholder="e.g. 12 months on ramps" className="bg-gray-800 border-gray-700" />
</div>
</CardContent>
</Card>
<LabExtrasFields
existingTags={existingProfile?.tags}
onChange={setExtraTags}

View file

@ -88,6 +88,8 @@ export default function BecomeEditorPage() {
const [titleInput, setTitleInput] = useState('');
const [prefilled, setPrefilled] = useState(false);
const [extraTags, setExtraTags] = useState<string[][]>([]);
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.
@ -108,6 +110,8 @@ export default function BecomeEditorPage() {
setImage(tag('image') ?? null);
setExperience(tag('experience') ?? '');
setRate(tag('rate') ?? '');
setTurnaround(tag('turnaround') ?? '');
setDeliverables(tag('deliverables') ?? '');
setPhone(tag('phone') ?? '');
setEmail(tag('email') ?? '');
setWebsite(tag('website') ?? '');
@ -228,6 +232,8 @@ export default function BecomeEditorPage() {
tags.push(['entity', entityType]);
for (const extra of extraTags) tags.push(extra);
if (turnaround.trim()) tags.push(['turnaround', turnaround.trim()]);
if (deliverables.trim()) tags.push(['deliverables', deliverables.trim()]);
if (entityType === 'collective') {
for (const pk of members) tags.push(['p', pk]);
if (crewRef) tags.push(['a', crewRef]);
@ -515,6 +521,20 @@ export default function BecomeEditorPage() {
{/* Optional Pro upgrade — free to list without it */}
<div className="mt-4">
<Card className="bg-gray-900 border-gray-700">
<CardContent className="p-4 space-y-4">
<Label className="text-base font-bold">{t('labUi.craftFields.detailsTitle')}</Label>
<div className="space-y-2">
<Label>{t('labUi.craftFields.turnaround')}</Label>
<Input value={turnaround} onChange={(e) => setTurnaround(e.target.value)} placeholder="e.g. 2–5 days" className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label>{t('labUi.craftFields.deliverables')}</Label>
<Input value={deliverables} onChange={(e) => setDeliverables(e.target.value)} placeholder="e.g. 4K master, vertical cut, captions" className="bg-gray-800 border-gray-700" />
</div>
</CardContent>
</Card>
<LabExtrasFields
existingTags={existingProfile?.tags}
onChange={setExtraTags}

View file

@ -83,6 +83,9 @@ export default function BecomeFilmerPage() {
const [titleInput, setTitleInput] = useState('');
const [prefilled, setPrefilled] = useState(false);
const [extraTags, setExtraTags] = useState<string[][]>([]);
const [turnaround, setTurnaround] = useState('');
const [deliverables, setDeliverables] = useState('');
const [drone, setDrone] = useState('');
const [existingGeohash, setExistingGeohash] = useState<string | undefined>(undefined);
// Prefill from an existing profile so this is an edit, not a blind overwrite.
@ -103,6 +106,9 @@ export default function BecomeFilmerPage() {
setImage(tag('image') ?? null);
setExperience(tag('experience') ?? '');
setRate(tag('rate') ?? '');
setTurnaround(tag('turnaround') ?? '');
setDeliverables(tag('deliverables') ?? '');
setDrone(tag('drone') ?? '');
setPhone(tag('phone') ?? '');
setEmail(tag('email') ?? '');
setWebsite(tag('website') ?? '');
@ -248,6 +254,9 @@ export default function BecomeFilmerPage() {
tags.push(['entity', entityType]);
for (const extra of extraTags) tags.push(extra);
if (turnaround.trim()) tags.push(['turnaround', turnaround.trim()]);
if (deliverables.trim()) tags.push(['deliverables', deliverables.trim()]);
if (drone.trim()) tags.push(['drone', drone.trim()]);
if (entityType === 'collective') {
for (const pk of members) tags.push(['p', pk]);
if (crewRef) tags.push(['a', crewRef]);
@ -584,6 +593,24 @@ export default function BecomeFilmerPage() {
</div>
{/* Optional Pro upgrade — free to list without it */}
<Card className="bg-gray-900 border-gray-700">
<CardContent className="p-4 space-y-4">
<Label className="text-base font-bold">{t('labUi.craftFields.detailsTitle')}</Label>
<div className="space-y-2">
<Label>{t('labUi.craftFields.turnaround')}</Label>
<Input value={turnaround} onChange={(e) => setTurnaround(e.target.value)} placeholder="e.g. 1–2 weeks" className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label>{t('labUi.craftFields.deliverables')}</Label>
<Input value={deliverables} onChange={(e) => setDeliverables(e.target.value)} placeholder="e.g. master + raw, 4K" className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label>{t('labUi.craftFields.drone')}</Label>
<Input value={drone} onChange={(e) => setDrone(e.target.value)} placeholder="e.g. FPV, Mavic 3, none" className="bg-gray-800 border-gray-700" />
</div>
</CardContent>
</Card>
<LabExtrasFields
existingTags={existingProfile?.tags}
onChange={setExtraTags}

View file

@ -142,6 +142,8 @@ export default function BecomePhotographerPage() {
const [titleInput, setTitleInput] = useState('');
const [prefilled, setPrefilled] = useState(false);
const [extraTags, setExtraTags] = useState<string[][]>([]);
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.
@ -167,6 +169,8 @@ export default function BecomePhotographerPage() {
setImage(tag('image') ?? null);
setExperience(tag('experience') ?? '');
setRate(tag('rate') ?? '');
setTurnaround(tag('turnaround') ?? '');
setDeliverables(tag('deliverables') ?? '');
setPhone(tag('phone') ?? '');
setEmail(tag('email') ?? '');
setWebsite(tag('website') ?? '');
@ -320,6 +324,8 @@ export default function BecomePhotographerPage() {
tags.push(['entity', entityType]);
for (const extra of extraTags) tags.push(extra);
if (turnaround.trim()) tags.push(['turnaround', turnaround.trim()]);
if (deliverables.trim()) tags.push(['deliverables', deliverables.trim()]);
if (entityType === 'collective') {
for (const pk of members) tags.push(['p', pk]);
if (crewRef) tags.push(['a', crewRef]);
@ -650,6 +656,20 @@ export default function BecomePhotographerPage() {
</div>
{/* Optional Pro upgrade — free to list without it */}
<Card className="bg-gray-900 border-gray-700">
<CardContent className="p-4 space-y-4">
<Label className="text-base font-bold">{t('labUi.craftFields.detailsTitle')}</Label>
<div className="space-y-2">
<Label>{t('labUi.craftFields.turnaround')}</Label>
<Input value={turnaround} onChange={(e) => setTurnaround(e.target.value)} placeholder="e.g. 48 hours" className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label>{t('labUi.craftFields.deliverables')}</Label>
<Input value={deliverables} onChange={(e) => setDeliverables(e.target.value)} placeholder="e.g. hi-res edits, RAWs, prints" className="bg-gray-800 border-gray-700" />
</div>
</CardContent>
</Card>
<LabExtrasFields
existingTags={existingProfile?.tags}
onChange={setExtraTags}

View file

@ -39,6 +39,7 @@ import {
} from '@/lib/labelKeys';
import { COACH_FORMATS, COACH_LEVELS, COACH_AGE_GROUPS, COACH_CREDENTIALS, coachOptionKey, coachRateCurrencyKey, coachRateUnitKey, type CoachOption } from '@/lib/coachOptions';
import { parseLabExtras } from '@/lib/labExtras';
import { CRAFT_DETAIL_FIELDS } from '@/lib/craftFields';
import { LabExtrasSection } from '@/components/LabExtrasSection';
import { sanitizeUrl, sanitizeTel, sanitizeEmail, urlDomain } from '@/lib/sanitizeUrl';
import { useTranslation } from '@/contexts/language';
@ -467,6 +468,24 @@ export default function CoachDetailPage() {
{/* Shared extras (all crafts): payments, languages, availability, portfolio */}
<LabExtrasSection extras={extras} accent={accent} profilePubkey={profile.pubkey} />
{/* Craft-specific freeform details (see lib/craftFields) */}
{(() => {
const fields = (CRAFT_DETAIL_FIELDS[profile.profileType] ?? []).filter((f) => pget(f.tag));
if (fields.length === 0) return null;
return (
<div className="mb-6 space-y-4">
{fields.map((f) => (
<div key={f.tag}>
<h4 className="text-xs font-bold text-[#ccff00] uppercase mb-1">{t(f.labelKey)}</h4>
<p className="text-sm text-gray-300 whitespace-pre-wrap">
{pget(f.tag) === 'true' ? t('common.yes') : pget(f.tag)}
</p>
</div>
))}
</div>
);
})()}
{/* In the Bag — photographer / filmer gear (freeform) */}
{(isPhotographer || isFilmer || isBuilder) && pget('gear') && (
<div className="mb-6">