Give each call its own page, and a form to answer it

A call now has its address — description, what the volunteer gets, the
commitment added up, level, closing date, project, technologies — and
the application form right there. The landing's call card leads to it
instead of scrolling to the general form, and shows the level in the
visitor's language rather than the API's `mid` or `any`.

The form is the landing's, with a `callSlug` that hides the kind
selector and posts to the new /api/calls/{slug}/apply handler: the API
refuses a `kind` on an application, which is always an offer of time.
Both handlers share one check, core/submission.ts, now tested on its
own.

A call that is filled or closed keeps its page — a link to it may
still be out there — but shows why there is no form, and where to
write instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pigui
2026-09-29 15:02:06 +02:00
parent 439f951f2e
commit b5681a12ac
15 changed files with 625 additions and 92 deletions

View File

@@ -114,6 +114,18 @@ export interface Copy {
links: string;
since: string;
};
call: {
eyebrow: string;
seniority: Record<'any' | 'junior' | 'mid' | 'senior', string>;
whatYouGet: string;
commitment: string;
totalHours: string;
project: string;
apply: string;
applyLead: string;
unavailable: string;
unavailableLead: string;
};
maintenance: { title: string; back: string; until: string };
}
@@ -236,6 +248,18 @@ const es: Copy = {
links: 'Enlaces',
since: 'En el colectivo desde',
},
call: {
eyebrow: 'Convocatoria',
seniority: { any: 'Cualquier nivel', junior: 'Junior', mid: 'Intermedio', senior: 'Senior' },
whatYouGet: 'Qué te llevas',
commitment: 'Dedicación',
totalHours: 'horas en total',
project: 'Proyecto',
apply: 'Apúntate',
applyLead: 'Cuéntanos quién eres y qué te gustaría hacer. Te respondemos personas, no un bot.',
unavailable: 'Esta convocatoria ya no está abierta',
unavailableLead: 'Si te interesa colaborar igualmente, escríbenos desde el formulario general.',
},
maintenance: {
title: 'Estamos de mantenimiento',
back: 'Volvemos en un rato.',
@@ -362,6 +386,18 @@ const en: Copy = {
links: 'Links',
since: 'In the collective since',
},
call: {
eyebrow: 'Open call',
seniority: { any: 'Any level', junior: 'Junior', mid: 'Mid-level', senior: 'Senior' },
whatYouGet: 'What you get',
commitment: 'Commitment',
totalHours: 'hours in total',
project: 'Project',
apply: 'Apply',
applyLead: 'Tell us who you are and what you would like to do. A person will answer, not a bot.',
unavailable: 'This call is no longer open',
unavailableLead: 'If you would like to help anyway, write to us from the general form.',
},
maintenance: {
title: 'We are under maintenance',
back: 'Back in a while.',
@@ -488,6 +524,18 @@ const ca: Copy = {
links: 'Enllaços',
since: 'Al col·lectiu des de',
},
call: {
eyebrow: 'Convocatòria',
seniority: { any: 'Qualsevol nivell', junior: 'Júnior', mid: 'Intermedi', senior: 'Sènior' },
whatYouGet: "Què t'emportes",
commitment: 'Dedicació',
totalHours: 'hores en total',
project: 'Projecte',
apply: "Apunta-t'hi",
applyLead: "Explica'ns qui ets i què t'agradaria fer. Et responen persones, no un bot.",
unavailable: 'Aquesta convocatòria ja no està oberta',
unavailableLead: "Si igualment t'interessa col·laborar, escriu-nos des del formulari general.",
},
maintenance: {
title: 'Estem de manteniment',
back: 'Tornem d’aquí una estona.',