diff --git a/AGENTS.md b/AGENTS.md index f5f725b..a461c81 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,8 +63,9 @@ session with **no other memory of this project**. So: (multi-letter, client-filtered like `experience`/`rate`): `format` (one-on-one/group/camp/video-analysis/contest-prep/park-lesson/adaptive/ girls-session/after-school), `level`, `age`, `cert` (self-attested - first-aid/cpr/safeguarding/background-check/insured/federation — **never shown - as verified**), `language` (free-form), `availability`, `booking` (URL), + first-aid/cpr/safeguarding/background-check/insured/federation/qualification/ + judge + free-form `cert_note` — **never shown as verified**), `language` + (free-form), `availability`, `booking` (URL), `photo` (repeatable gallery, ≤12) and `video` (YouTube/Instagram reel). Option sets + i18n keys live in **`src/lib/coachOptions.ts`**; the form renders a reusable `OptionChips` row; the gallery reuses **`PhotoDropzone`** + diff --git a/NIP.md b/NIP.md index d03de1d..403bdd7 100644 --- a/NIP.md +++ b/NIP.md @@ -2063,7 +2063,8 @@ client-side. | `format` | `one-on-one`, `group`, `camp`, `video-analysis`, `contest-prep`, `park-lesson`, `adaptive`, `girls-session`, `after-school` | How sessions are delivered | | `level` | `beginner`, `intermediate`, `advanced`, `competition` | Levels taught | | `age` | `kids`, `teens`, `adults`, `seniors` | Age groups coached | -| `cert` | `first-aid`, `cpr`, `safeguarding`, `background-check`, `insured`, `federation` | Self-attested credentials / safety | +| `cert` | `first-aid`, `cpr`, `safeguarding`, `background-check`, `insured`, `federation`, `qualification`, `judge` | Self-attested credentials / safety | +| `cert_note` | free-form | Optional — names an exact certification / issuer | | `language` | free-form (e.g. `English`, `Español`) | Languages spoken | | `availability` | `weekdays`, `weekends`, `evenings`, `after-school`, `school-holidays`, `seasonal` | When available | | `booking` | URL | Optional booking / scheduling link | diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 3d1a5af..fe33a8c 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -2209,7 +2209,11 @@ labUi: { certSafeguarding: 'Safeguarding', certBackgroundCheck: 'Background check', certInsured: 'Insured', - certFederation: 'Federation coach', + certFederation: 'National federation coach', + certQualification: 'Coaching qualification', + certJudge: 'Competition judge', + certNoteLabel: 'Other credentials (optional)', + certNotePlaceholder: 'e.g. Skateboard GB Level 2, 8 years coaching…', availWeekdays: 'Weekdays', availWeekends: 'Weekends', availEvenings: 'Evenings', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 4e1f046..52a1b08 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -2209,7 +2209,11 @@ labUi: { certSafeguarding: 'Protección infantil', certBackgroundCheck: 'Antecedentes penales', certInsured: 'Asegurado', - certFederation: 'Entrenador federativo', + certFederation: 'Entrenador federativo nacional', + certQualification: 'Titulación de entrenador', + certJudge: 'Juez de competición', + certNoteLabel: 'Otras credenciales (opcional)', + certNotePlaceholder: 'p. ej. Nivel 2 de Skateboard GB, 8 años entrenando…', availWeekdays: 'Entre semana', availWeekends: 'Fines de semana', availEvenings: 'Tardes/noches', diff --git a/src/lib/coachOptions.ts b/src/lib/coachOptions.ts index 912f308..432d99a 100644 --- a/src/lib/coachOptions.ts +++ b/src/lib/coachOptions.ts @@ -40,6 +40,8 @@ export const COACH_CREDENTIALS: CoachOption[] = [ { value: 'background-check', key: 'labUi.coachForm.certBackgroundCheck' }, { value: 'insured', key: 'labUi.coachForm.certInsured' }, { value: 'federation', key: 'labUi.coachForm.certFederation' }, + { value: 'qualification', key: 'labUi.coachForm.certQualification' }, + { value: 'judge', key: 'labUi.coachForm.certJudge' }, ]; export const COACH_AVAILABILITY: CoachOption[] = [ diff --git a/src/lib/types.ts b/src/lib/types.ts index c1eb62a..8090e45 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -2343,6 +2343,8 @@ export interface SkateCoach { ageGroups: string[]; /** Self-attested credentials (first-aid, safeguarding, …). */ credentials: string[]; + /** Optional free-form credential note (names an exact cert / issuer). */ + certNote?: string; /** Languages spoken. */ languages: string[]; /** Availability windows (weekends, evenings, …). */ @@ -2692,6 +2694,7 @@ export function parseSkateCoach(event: NostrEvent): SkateCoach | null { const levels = values('level'); const ageGroups = values('age'); const credentials = values('cert'); + const certNote = event.tags.find(([name]) => name === 'cert_note')?.[1]; // Languages are proper nouns — preserve case (no lowercasing). const languages = event.tags .filter(([n, v]) => n === 'language' && !!v?.trim()) @@ -2753,6 +2756,7 @@ export function parseSkateCoach(event: NostrEvent): SkateCoach | null { levels, ageGroups, credentials, + certNote, languages, availability, booking, diff --git a/src/pages/BecomeCoachPage.tsx b/src/pages/BecomeCoachPage.tsx index 9260c00..4f352e5 100644 --- a/src/pages/BecomeCoachPage.tsx +++ b/src/pages/BecomeCoachPage.tsx @@ -140,6 +140,7 @@ export default function BecomeCoachPage() { const [levels, setLevels] = useState([]); const [ageGroups, setAgeGroups] = useState([]); const [credentials, setCredentials] = useState([]); + const [certNote, setCertNote] = useState(''); const [languages, setLanguages] = useState([]); const [languageInput, setLanguageInput] = useState(''); const [availability, setAvailability] = useState([]); @@ -187,6 +188,7 @@ export default function BecomeCoachPage() { setLevels(tags('level')); setAgeGroups(tags('age')); setCredentials(tags('cert')); + setCertNote(tag('cert_note') ?? ''); setLanguages(tags('language')); setAvailability(tags('availability')); setBooking(tag('booking') ?? ''); @@ -360,6 +362,7 @@ export default function BecomeCoachPage() { for (const v of levels) tags.push(['level', v]); for (const v of ageGroups) tags.push(['age', v]); for (const v of credentials) tags.push(['cert', v]); + if (certNote.trim()) tags.push(['cert_note', certNote.trim()]); for (const v of languages) tags.push(['language', v]); for (const v of availability) tags.push(['availability', v]); if (booking.trim()) tags.push(['booking', normalizeUrlInput(booking)]); @@ -619,6 +622,11 @@ export default function BecomeCoachPage() { idPrefix="cert" /> +
+ + setCertNote(e.target.value)} className="bg-gray-800 border-gray-700" /> +
+

{t('labUi.coachForm.languagesHint')}

diff --git a/src/pages/CoachDetailPage.tsx b/src/pages/CoachDetailPage.tsx index 4c7403a..49b156c 100644 --- a/src/pages/CoachDetailPage.tsx +++ b/src/pages/CoachDetailPage.tsx @@ -425,21 +425,26 @@ export default function CoachDetailPage() { )} {/* Coach — credentials, formats, levels, ages, languages, availability */} - {isCoach && (parr('credentials').length > 0 || parr('formats').length > 0 || parr('levels').length > 0 || parr('ageGroups').length > 0 || parr('languages').length > 0 || parr('availability').length > 0 || pget('booking')) && ( + {isCoach && (parr('credentials').length > 0 || pget('certNote') || parr('formats').length > 0 || parr('levels').length > 0 || parr('ageGroups').length > 0 || parr('languages').length > 0 || parr('availability').length > 0 || pget('booking')) && (
- {parr('credentials').length > 0 && ( + {(parr('credentials').length > 0 || pget('certNote')) && (

{t('labUi.detail.credentialsTitle')}

-
- {parr('credentials').map((c) => ( - - - {optLabel(COACH_CREDENTIALS, c)} - - ))} -
+ {parr('credentials').length > 0 && ( +
+ {parr('credentials').map((c) => ( + + + {optLabel(COACH_CREDENTIALS, c)} + + ))} +
+ )} + {pget('certNote') && ( +

{pget('certNote')}

+ )}

{t('labUi.detail.selfAttested')}

)}