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:
parent
88a3e4d1b4
commit
681eab9592
12 changed files with 251 additions and 3 deletions
|
|
@ -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
17
NIP.md
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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] ?? ''}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
64
src/lib/craftFields.ts
Normal 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' },
|
||||
],
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue