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.
140 lines
5.3 KiB
TypeScript
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;
|
|
}
|