SkateSpots/src/lib/labExtras.ts
SkateSpots 81b5fd0fab Add a live profile preview to every Lab offer form
Filling the long form was blind until publish. LabProfilePreview renders a
faithful profile card from the current draft — title, bio, image, location,
skills, structured rate, languages/availability/payments and gallery — with a
not-published-yet note.

- Wired into CraftProfileForm (8 crafts) and the coach + filmer/editor/
  photographer/builder pages via a Preview button next to Submit.
- parseLabExtras now accepts any { tags } so a draft tag list can be previewed
  without a published event.
- i18n for the preview copy (EN/ES).

Still open: draft/autosave and a multi-step layout.
2026-10-04 11:25:58 +07:00

140 lines
5.3 KiB
TypeScript

/**
* 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;
}