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`
|
(multi-letter, client-filtered like `experience`/`rate`): `format`
|
||||||
(one-on-one/group/camp/video-analysis/contest-prep/park-lesson/adaptive/
|
(one-on-one/group/camp/video-analysis/contest-prep/park-lesson/adaptive/
|
||||||
girls-session/after-school), `level`, `age`, `cert` (self-attested
|
girls-session/after-school), `level`, `age`, `cert` (self-attested
|
||||||
first-aid/cpr/safeguarding/background-check/insured/federation — **never shown
|
first-aid/cpr/safeguarding/background-check/insured/federation/qualification/
|
||||||
as verified**), `language` (free-form), `availability`, `booking` (URL),
|
judge + free-form `cert_note` — **never shown as verified**), `language`
|
||||||
|
(free-form), `availability`, `booking` (URL),
|
||||||
`photo` (repeatable gallery, ≤12) and `video` (YouTube/Instagram reel).
|
`photo` (repeatable gallery, ≤12) and `video` (YouTube/Instagram reel).
|
||||||
Option sets + i18n keys live in **`src/lib/coachOptions.ts`**; the form renders
|
Option sets + i18n keys live in **`src/lib/coachOptions.ts`**; the form renders
|
||||||
a reusable `OptionChips` row; the gallery reuses **`PhotoDropzone`** +
|
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 |
|
| `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 |
|
| `level` | `beginner`, `intermediate`, `advanced`, `competition` | Levels taught |
|
||||||
| `age` | `kids`, `teens`, `adults`, `seniors` | Age groups coached |
|
| `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 |
|
| `language` | free-form (e.g. `English`, `Español`) | Languages spoken |
|
||||||
| `availability` | `weekdays`, `weekends`, `evenings`, `after-school`, `school-holidays`, `seasonal` | When available |
|
| `availability` | `weekdays`, `weekends`, `evenings`, `after-school`, `school-holidays`, `seasonal` | When available |
|
||||||
| `booking` | URL | Optional booking / scheduling link |
|
| `booking` | URL | Optional booking / scheduling link |
|
||||||
|
|
|
||||||
|
|
@ -2209,7 +2209,11 @@ labUi: {
|
||||||
certSafeguarding: 'Safeguarding',
|
certSafeguarding: 'Safeguarding',
|
||||||
certBackgroundCheck: 'Background check',
|
certBackgroundCheck: 'Background check',
|
||||||
certInsured: 'Insured',
|
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',
|
availWeekdays: 'Weekdays',
|
||||||
availWeekends: 'Weekends',
|
availWeekends: 'Weekends',
|
||||||
availEvenings: 'Evenings',
|
availEvenings: 'Evenings',
|
||||||
|
|
|
||||||
|
|
@ -2209,7 +2209,11 @@ labUi: {
|
||||||
certSafeguarding: 'Protección infantil',
|
certSafeguarding: 'Protección infantil',
|
||||||
certBackgroundCheck: 'Antecedentes penales',
|
certBackgroundCheck: 'Antecedentes penales',
|
||||||
certInsured: 'Asegurado',
|
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',
|
availWeekdays: 'Entre semana',
|
||||||
availWeekends: 'Fines de semana',
|
availWeekends: 'Fines de semana',
|
||||||
availEvenings: 'Tardes/noches',
|
availEvenings: 'Tardes/noches',
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ export const COACH_CREDENTIALS: CoachOption[] = [
|
||||||
{ value: 'background-check', key: 'labUi.coachForm.certBackgroundCheck' },
|
{ value: 'background-check', key: 'labUi.coachForm.certBackgroundCheck' },
|
||||||
{ value: 'insured', key: 'labUi.coachForm.certInsured' },
|
{ value: 'insured', key: 'labUi.coachForm.certInsured' },
|
||||||
{ value: 'federation', key: 'labUi.coachForm.certFederation' },
|
{ value: 'federation', key: 'labUi.coachForm.certFederation' },
|
||||||
|
{ value: 'qualification', key: 'labUi.coachForm.certQualification' },
|
||||||
|
{ value: 'judge', key: 'labUi.coachForm.certJudge' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const COACH_AVAILABILITY: CoachOption[] = [
|
export const COACH_AVAILABILITY: CoachOption[] = [
|
||||||
|
|
|
||||||
|
|
@ -2343,6 +2343,8 @@ export interface SkateCoach {
|
||||||
ageGroups: string[];
|
ageGroups: string[];
|
||||||
/** Self-attested credentials (first-aid, safeguarding, …). */
|
/** Self-attested credentials (first-aid, safeguarding, …). */
|
||||||
credentials: string[];
|
credentials: string[];
|
||||||
|
/** Optional free-form credential note (names an exact cert / issuer). */
|
||||||
|
certNote?: string;
|
||||||
/** Languages spoken. */
|
/** Languages spoken. */
|
||||||
languages: string[];
|
languages: string[];
|
||||||
/** Availability windows (weekends, evenings, …). */
|
/** Availability windows (weekends, evenings, …). */
|
||||||
|
|
@ -2692,6 +2694,7 @@ export function parseSkateCoach(event: NostrEvent): SkateCoach | null {
|
||||||
const levels = values('level');
|
const levels = values('level');
|
||||||
const ageGroups = values('age');
|
const ageGroups = values('age');
|
||||||
const credentials = values('cert');
|
const credentials = values('cert');
|
||||||
|
const certNote = event.tags.find(([name]) => name === 'cert_note')?.[1];
|
||||||
// Languages are proper nouns — preserve case (no lowercasing).
|
// Languages are proper nouns — preserve case (no lowercasing).
|
||||||
const languages = event.tags
|
const languages = event.tags
|
||||||
.filter(([n, v]) => n === 'language' && !!v?.trim())
|
.filter(([n, v]) => n === 'language' && !!v?.trim())
|
||||||
|
|
@ -2753,6 +2756,7 @@ export function parseSkateCoach(event: NostrEvent): SkateCoach | null {
|
||||||
levels,
|
levels,
|
||||||
ageGroups,
|
ageGroups,
|
||||||
credentials,
|
credentials,
|
||||||
|
certNote,
|
||||||
languages,
|
languages,
|
||||||
availability,
|
availability,
|
||||||
booking,
|
booking,
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ export default function BecomeCoachPage() {
|
||||||
const [levels, setLevels] = useState<string[]>([]);
|
const [levels, setLevels] = useState<string[]>([]);
|
||||||
const [ageGroups, setAgeGroups] = useState<string[]>([]);
|
const [ageGroups, setAgeGroups] = useState<string[]>([]);
|
||||||
const [credentials, setCredentials] = useState<string[]>([]);
|
const [credentials, setCredentials] = useState<string[]>([]);
|
||||||
|
const [certNote, setCertNote] = useState('');
|
||||||
const [languages, setLanguages] = useState<string[]>([]);
|
const [languages, setLanguages] = useState<string[]>([]);
|
||||||
const [languageInput, setLanguageInput] = useState('');
|
const [languageInput, setLanguageInput] = useState('');
|
||||||
const [availability, setAvailability] = useState<string[]>([]);
|
const [availability, setAvailability] = useState<string[]>([]);
|
||||||
|
|
@ -187,6 +188,7 @@ export default function BecomeCoachPage() {
|
||||||
setLevels(tags('level'));
|
setLevels(tags('level'));
|
||||||
setAgeGroups(tags('age'));
|
setAgeGroups(tags('age'));
|
||||||
setCredentials(tags('cert'));
|
setCredentials(tags('cert'));
|
||||||
|
setCertNote(tag('cert_note') ?? '');
|
||||||
setLanguages(tags('language'));
|
setLanguages(tags('language'));
|
||||||
setAvailability(tags('availability'));
|
setAvailability(tags('availability'));
|
||||||
setBooking(tag('booking') ?? '');
|
setBooking(tag('booking') ?? '');
|
||||||
|
|
@ -360,6 +362,7 @@ export default function BecomeCoachPage() {
|
||||||
for (const v of levels) tags.push(['level', v]);
|
for (const v of levels) tags.push(['level', v]);
|
||||||
for (const v of ageGroups) tags.push(['age', v]);
|
for (const v of ageGroups) tags.push(['age', v]);
|
||||||
for (const v of credentials) tags.push(['cert', 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 languages) tags.push(['language', v]);
|
||||||
for (const v of availability) tags.push(['availability', v]);
|
for (const v of availability) tags.push(['availability', v]);
|
||||||
if (booking.trim()) tags.push(['booking', normalizeUrlInput(booking)]);
|
if (booking.trim()) tags.push(['booking', normalizeUrlInput(booking)]);
|
||||||
|
|
@ -619,6 +622,11 @@ export default function BecomeCoachPage() {
|
||||||
idPrefix="cert"
|
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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="language">{t('labUi.coachForm.languagesLabel')}</Label>
|
<Label htmlFor="language">{t('labUi.coachForm.languagesLabel')}</Label>
|
||||||
<p className="text-xs text-gray-500">{t('labUi.coachForm.languagesHint')}</p>
|
<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 */}
|
{/* 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">
|
<div className="mb-6 space-y-5">
|
||||||
{parr('credentials').length > 0 && (
|
{(parr('credentials').length > 0 || pget('certNote')) && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-[#ccff00] uppercase mb-2 flex items-center gap-2">
|
<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')}
|
<ShieldCheck className={`h-4 w-4 ${accent.text}`} /> {t('labUi.detail.credentialsTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
{parr('credentials').length > 0 && (
|
||||||
{parr('credentials').map((c) => (
|
<div className="flex flex-wrap gap-2">
|
||||||
<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`}>
|
{parr('credentials').map((c) => (
|
||||||
<ShieldCheck className="h-3.5 w-3.5" />
|
<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`}>
|
||||||
{optLabel(COACH_CREDENTIALS, c)}
|
<ShieldCheck className="h-3.5 w-3.5" />
|
||||||
</span>
|
{optLabel(COACH_CREDENTIALS, c)}
|
||||||
))}
|
</span>
|
||||||
</div>
|
))}
|
||||||
|
</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>
|
<p className="text-xs text-gray-500 mt-1.5">{t('labUi.detail.selfAttested')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue