/** * Shared "extras" for every Lab provider profile (all craft kinds). * * The coach profile pioneered these fields; rather than copy them 12 times, * this module defines ONE parser + option sets that every craft's form and the * shared detail page use. The tags are identical across crafts: * * language / availability / rate_amount / rate_currency / rate_unit / * payment_method / payment_note / booking_fee / booking_fee_currency / * pay_what_you_can / photo / video / booking * * Craft-specific *context* stays in each form's `skillSections` + `extraFields`; * this is the craft-agnostic enrichment (how to reach them, how to pay, media). */ import { COACH_PAYMENT_METHODS } from '@/lib/coachOptions'; /** A stored value + its i18n key. */ export interface LabOption { value: string; key: string; } /** Currencies a provider can price in (sats first — it's a Bitcoin app). */ export const RATE_CURRENCIES: LabOption[] = [ { value: 'sats', key: 'labUi.coachForm.rateCurrencySats' }, { value: 'USD', key: 'labUi.coachForm.rateCurrencyUsd' }, { value: 'EUR', key: 'labUi.coachForm.rateCurrencyEur' }, { value: 'GBP', key: 'labUi.coachForm.rateCurrencyGbp' }, ]; /** Every billing unit any craft might use (forms pick a subset by context). */ export const ALL_RATE_UNITS: LabOption[] = [ { value: 'hour', key: 'labUi.coachForm.perHour' }, { value: 'session', key: 'labUi.coachForm.perSession' }, { value: 'half-day', key: 'labUi.coachForm.perHalfDay' }, { value: 'day', key: 'labUi.coachForm.perDay' }, { value: 'project', key: 'labUi.coachForm.perProject' }, { value: 'event', key: 'labUi.coachForm.perEvent' }, { value: 'gig', key: 'labUi.coachForm.perGig' }, { value: 'shoot', key: 'labUi.coachForm.perShoot' }, { value: 'piece', key: 'labUi.coachForm.perPiece' }, { value: 'cut', key: 'labUi.coachForm.perCut' }, { value: 'month', key: 'labUi.coachForm.perMonth' }, ]; /** Sensible default units when a craft doesn't specify its own. */ export const DEFAULT_RATE_UNITS: LabOption[] = ALL_RATE_UNITS.filter((u) => ['hour', 'session', 'half-day', 'day', 'project'].includes(u.value), ); /** Pick a craft-specific subset of billing units, in the canonical order. */ export function pickRateUnits(...values: string[]): LabOption[] { return ALL_RATE_UNITS.filter((u) => values.includes(u.value)); } /** Generic availability windows — union of coach + craft conventions. */ export const AVAILABILITY_OPTIONS: LabOption[] = [ { value: 'weekdays', key: 'labUi.coachForm.availWeekdays' }, { value: 'weekends', key: 'labUi.coachForm.availWeekends' }, { value: 'evenings', key: 'labUi.coachForm.availEvenings' }, { value: 'after-school', key: 'labUi.coachForm.availAfterSchool' }, { value: 'school-holidays', key: 'labUi.coachForm.availHolidays' }, { value: 'by-appointment', key: 'labUi.coachForm.availByAppointment' }, { value: 'seasonal', key: 'labUi.coachForm.availSeasonal' }, ]; /** Accepted payment methods (shared with the coach profile). */ export const PAYMENT_METHODS = COACH_PAYMENT_METHODS; /** The craft-agnostic enrichment parsed off any Lab provider event. */ export interface LabExtras { languages: string[]; availability: string[]; rateAmount?: string; rateCurrency?: string; rateUnit?: string; paymentMethods: string[]; paymentNote?: string; bookingFee?: string; bookingFeeCurrency?: string; payWhatYouCan: boolean; photos: string[]; video?: string; booking?: string; // Coach-origin facets (kept here so the Lab filter reads one source for all // crafts; other crafts simply leave them empty). formats: string[]; levels: string[]; ageGroups: string[]; credentials: string[]; certNote?: string; } /** Read the shared extras off any Lab provider event (craft-agnostic). Accepts * anything with a `tags` array, so draft/preview tag lists work too. */ export function parseLabExtras(event: { tags: string[][] }): LabExtras { const tag = (n: string) => event.tags.find(([t]) => t === n)?.[1]; const vals = (n: string, lower = true): string[] => [ ...new Set( event.tags .filter(([t, v]) => t === n && !!v?.trim()) .map(([, v]) => (lower ? v.trim().toLowerCase() : v.trim())), ), ]; return { languages: vals('language', false), availability: vals('availability'), rateAmount: tag('rate_amount'), rateCurrency: tag('rate_currency'), rateUnit: tag('rate_unit'), paymentMethods: vals('payment_method'), paymentNote: tag('payment_note'), bookingFee: tag('booking_fee'), bookingFeeCurrency: tag('booking_fee_currency'), payWhatYouCan: event.tags.some(([t, v]) => t === 'pay_what_you_can' && v === 'true'), photos: event.tags .filter(([t, v]) => t === 'photo' && !!v?.trim()) .map(([, v]) => v.trim()) .filter((v, i, arr) => arr.indexOf(v) === i) .slice(0, 12), video: tag('video'), booking: tag('booking'), formats: vals('format'), levels: vals('level'), ageGroups: vals('age'), credentials: vals('cert'), certNote: tag('cert_note'), }; } /** Resolve a stored option value to its translated label (raw value fallback). */ export function labOptionLabel( options: LabOption[], value: string, t: (key: string) => string, ): string { const key = options.find((o) => o.value === value)?.key; return key ? t(key) : value; }