Expand coach credentials with skate roles + a free-text note
- Rename 'Federation coach' -> 'National federation coach'. - Add 'Coaching qualification' and 'Competition judge'. - Add a free-form cert_note so a coach can name an exact cert/issuer. The core safety chips (first aid, CPR, safeguarding, background check, insured) stay - they're the load-bearing trust signals. cert_note is displayed under the chips and included in the same self-attested disclaimer.
This commit is contained in:
parent
2db13612d1
commit
357d042a13
8 changed files with 44 additions and 15 deletions
|
|
@ -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`** +
|
||||
|
|
|
|||
3
NIP.md
3
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 |
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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[] = [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ export default function BecomeCoachPage() {
|
|||
const [levels, setLevels] = useState<string[]>([]);
|
||||
const [ageGroups, setAgeGroups] = useState<string[]>([]);
|
||||
const [credentials, setCredentials] = useState<string[]>([]);
|
||||
const [certNote, setCertNote] = useState('');
|
||||
const [languages, setLanguages] = useState<string[]>([]);
|
||||
const [languageInput, setLanguageInput] = useState('');
|
||||
const [availability, setAvailability] = useState<string[]>([]);
|
||||
|
|
@ -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"
|
||||
/>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="cert-note" className="text-xs font-normal text-gray-400">{t('labUi.coachForm.certNoteLabel')}</Label>
|
||||
<Input id="cert-note" placeholder={t('labUi.coachForm.certNotePlaceholder')} value={certNote} onChange={(e) => setCertNote(e.target.value)} className="bg-gray-800 border-gray-700" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language">{t('labUi.coachForm.languagesLabel')}</Label>
|
||||
<p className="text-xs text-gray-500">{t('labUi.coachForm.languagesHint')}</p>
|
||||
|
|
|
|||
|
|
@ -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')) && (
|
||||
<div className="mb-6 space-y-5">
|
||||
{parr('credentials').length > 0 && (
|
||||
{(parr('credentials').length > 0 || pget('certNote')) && (
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-[#ccff00] uppercase mb-2 flex items-center gap-2">
|
||||
<ShieldCheck className={`h-4 w-4 ${accent.text}`} /> {t('labUi.detail.credentialsTitle')}
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{parr('credentials').map((c) => (
|
||||
<span key={c} className={`inline-flex items-center gap-1.5 text-xs font-bold px-3 py-1.5 rounded-full border ${accent.border} text-gray-200`}>
|
||||
<ShieldCheck className="h-3.5 w-3.5" />
|
||||
{optLabel(COACH_CREDENTIALS, c)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{parr('credentials').length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{parr('credentials').map((c) => (
|
||||
<span key={c} className={`inline-flex items-center gap-1.5 text-xs font-bold px-3 py-1.5 rounded-full border ${accent.border} text-gray-200`}>
|
||||
<ShieldCheck className="h-3.5 w-3.5" />
|
||||
{optLabel(COACH_CREDENTIALS, c)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{pget('certNote') && (
|
||||
<p className="mt-2 text-sm text-gray-300">{pget('certNote')}</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 mt-1.5">{t('labUi.detail.selfAttested')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue