Add a page for every way to help

/colabora (/en/contribute, /ca/col-labora) — the calls' own section, so
it lists what /colabora/{slug} are — brings the open calls, the good
first issues across the collective's repositories, and a five-step
guide from picking a task to having it reviewed. With nothing open and
nothing labelled, the guide and the other ways to help are still there.
The landing's "contribute" section links to it.

Also fixes the landing's issue list, which @pi/ui's `li + li` margin
spaced twice over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pigui
2026-09-29 15:14:16 +02:00
parent daacc2c5d6
commit 399b8d5450
11 changed files with 370 additions and 3 deletions

View File

@@ -114,6 +114,20 @@ export interface Copy {
links: string;
since: string;
};
contribute: {
eyebrow: string;
title: string;
lead: string;
calls: string;
callsLead: string;
issues: string;
issuesLead: string;
noIssues: string;
guide: string;
steps: { title: string; text: string }[];
otherWays: string;
otherWaysLead: string;
};
service: {
eyebrow: string;
projects: string;
@@ -267,6 +281,26 @@ const es: Copy = {
links: 'Enlaces',
since: 'En el colectivo desde',
},
contribute: {
eyebrow: 'Colabora',
title: 'Tu primera contribución',
lead: 'No hace falta ser experta ni tener mucho tiempo. Elige algo pequeño, pregunta lo que necesites y te acompañamos hasta que tu cambio esté publicado.',
calls: 'Convocatorias abiertas',
callsLead: 'Proyectos que buscan manos ahora mismo, con la dedicación semanal clara.',
issues: 'Tareas para empezar',
issuesLead: 'Issues de nuestros repositorios pensadas para quien llega por primera vez.',
noIssues: 'Ahora mismo no hay tareas etiquetadas para empezar. Escríbenos y te buscamos una.',
guide: 'Cómo empezar',
steps: [
{ title: 'Elige una tarea', text: 'Busca una etiquetada como «good first issue»: son acotadas y están bien explicadas.' },
{ title: 'Dilo en la issue', text: 'Deja un comentario para que nadie trabaje en lo mismo. Si tienes dudas, pregúntalas ahí.' },
{ title: 'Haz tu cambio', text: 'Crea una rama desde la principal, haz el cambio y comprueba que los tests pasan.' },
{ title: 'Abre una pull request', text: 'Explica qué has cambiado y por qué. Enlaza la issue para que se cierre sola.' },
{ title: 'Revisión', text: 'Alguien del colectivo revisará tu código contigo. Es una conversación, no un examen.' },
],
otherWays: 'Otras formas de ayudar',
otherWaysLead: 'También necesitamos diseño, documentación, traducciones y gente que pruebe lo que hacemos. Escríbenos y lo hablamos.',
},
service: {
eyebrow: 'Servicio',
projects: 'Trabajo relacionado',
@@ -433,6 +467,26 @@ const en: Copy = {
links: 'Links',
since: 'In the collective since',
},
contribute: {
eyebrow: 'Contribute',
title: 'Your first contribution',
lead: 'You do not need to be an expert or have lots of time. Pick something small, ask whatever you need and we will help you all the way until your change is live.',
calls: 'Open calls',
callsLead: 'Projects looking for hands right now, with the weekly commitment stated up front.',
issues: 'Good first issues',
issuesLead: 'Issues in our repositories meant for people contributing for the first time.',
noIssues: 'There are no issues labelled for newcomers right now. Write to us and we will find you one.',
guide: 'How to get started',
steps: [
{ title: 'Pick a task', text: 'Look for one labelled "good first issue": they are well scoped and clearly explained.' },
{ title: 'Say so on the issue', text: 'Leave a comment so nobody works on the same thing. If you have questions, ask them there.' },
{ title: 'Make your change', text: 'Branch off the main branch, make the change and check that the tests pass.' },
{ title: 'Open a pull request', text: 'Explain what you changed and why. Link the issue so it closes on its own.' },
{ title: 'Review', text: 'Someone from the collective will go through your code with you. It is a conversation, not an exam.' },
],
otherWays: 'Other ways to help',
otherWaysLead: 'We also need design, documentation, translations and people to try what we build. Write to us and we will talk it over.',
},
service: {
eyebrow: 'Service',
projects: 'Related work',
@@ -599,6 +653,26 @@ const ca: Copy = {
links: 'Enllaços',
since: 'Al col·lectiu des de',
},
contribute: {
eyebrow: 'Col·labora',
title: 'La teva primera contribució',
lead: "No cal ser experta ni tenir gaire temps. Tria alguna cosa petita, pregunta el que necessitis i t'acompanyem fins que el teu canvi estigui publicat.",
calls: 'Convocatòries obertes',
callsLead: 'Projectes que busquen mans ara mateix, amb la dedicació setmanal clara.',
issues: 'Tasques per començar',
issuesLead: 'Issues dels nostres repositoris pensades per a qui arriba per primera vegada.',
noIssues: "Ara mateix no hi ha tasques etiquetades per començar. Escriu-nos i te'n buscarem una.",
guide: 'Com començar',
steps: [
{ title: 'Tria una tasca', text: "Busca'n una etiquetada com a «good first issue»: són acotades i estan ben explicades." },
{ title: 'Digues-ho a la issue', text: 'Deixa un comentari perquè ningú treballi en el mateix. Si tens dubtes, pregunta-ho allà.' },
{ title: 'Fes el teu canvi', text: 'Crea una branca des de la principal, fes el canvi i comprova que els tests passen.' },
{ title: 'Obre una pull request', text: 'Explica què has canviat i per què. Enllaça la issue perquè es tanqui sola.' },
{ title: 'Revisió', text: 'Algú del col·lectiu revisarà el teu codi amb tu. És una conversa, no un examen.' },
],
otherWays: "Altres maneres d'ajudar",
otherWaysLead: 'També necessitem disseny, documentació, traduccions i gent que provi el que fem. Escriu-nos i en parlem.',
},
service: {
eyebrow: 'Servei',
projects: 'Feina relacionada',

View File

@@ -15,6 +15,7 @@ import {
} from './api';
import type {
PublicCall,
PublicIssue,
PublicMember,
PublicPage,
PublicProject,
@@ -30,7 +31,11 @@ export type RoutedRecord =
| { type: 'member'; record: PublicMember }
| { type: 'call'; record: PublicCall }
| { type: 'service'; record: PublicService }
| { type: 'radar'; record: PublicTechnology[] };
| { type: 'radar'; record: PublicTechnology[] }
| {
type: 'contribute';
record: { calls: PublicCall[]; issues: PublicIssue[] };
};
export interface RoutedPageData {
locale: Locale;
@@ -87,6 +92,17 @@ async function fetchRecord(route: RouteMatch): Promise<RoutedRecord | null> {
const { data } = await pireonApi.service(locale, slug);
return data ? { type: 'service', record: data } : null;
}
case 'contribute': {
const [calls, issues] = await Promise.all([
pireonApi.calls(locale),
pireonApi.goodFirstIssues({ limit: 30 }),
]);
// Nothing open is still an answer: the page says how to help anyway.
return {
type: 'contribute',
record: { calls: calls.data ?? [], issues: issues.data ?? [] },
};
}
case 'radar': {
// An empty radar is still the radar: the page says so rather than 404.
const { data, error } = await pireonApi.radar(locale);

View File

@@ -108,6 +108,17 @@ describe('listings', () => {
ca: '/ca/radar',
});
});
// The calls' section with nothing after it is the list of ways to help.
it('reads a call section on its own as the contribute listing', () => {
expect(resolveRoute('/colabora')).toEqual({
type: 'contribute',
locale: 'es',
slug: '',
});
expect(resolveRoute('/en/contribute')?.type).toBe('contribute');
expect(resolveRoute('/en/contribute/frontend')?.type).toBe('call');
});
});
describe('homeHref', () => {

View File

@@ -16,7 +16,7 @@ import { DEFAULT_LOCALE, SUPPORTED_LOCALES, type Locale } from './api';
export type RecordType = 'page' | 'project' | 'member' | 'call' | 'service';
/** Pages this site composes itself rather than one record's: no slug. */
export type ListingType = 'radar';
export type ListingType = 'radar' | 'contribute';
/**
* The section each type lives under, per language. The API's own
@@ -39,6 +39,8 @@ export const SECTIONS: Record<
*/
export const LISTINGS: Record<ListingType, Record<Locale, string>> = {
radar: { es: 'radar', en: 'radar', ca: 'radar' },
// The calls' own section, so /colabora lists what /colabora/{slug} are.
contribute: { es: 'colabora', en: 'contribute', ca: 'col-labora' },
};
export interface RouteMatch {