Give the tech radar its own page
/radar (/en/radar, /ca/radar) shows every technology the collective has placed, by ring: logo, category, what it is, why it sits there, its website and how many projects use it. The landing's radar summary links to it. It is the first listing — a page this site composes rather than one record's — so resolveRoute learns about those (LISTINGS), and builds their addresses and hreflang alternates from the same table. Logos sit on a light tile in both themes. A brand's mark is not ours to recolour, and Rust's, which is black, would vanish in dark mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -114,6 +114,19 @@ export interface Copy {
|
||||
links: string;
|
||||
since: string;
|
||||
};
|
||||
radar: {
|
||||
eyebrow: string;
|
||||
lead: string;
|
||||
ringLead: Record<'adopt' | 'trial' | 'assess' | 'hold', string>;
|
||||
categories: Record<
|
||||
'language' | 'framework' | 'datastore' | 'infrastructure' | 'tool' | 'practice',
|
||||
string
|
||||
>;
|
||||
website: string;
|
||||
projects: string;
|
||||
empty: string;
|
||||
seeAll: string;
|
||||
};
|
||||
call: {
|
||||
eyebrow: string;
|
||||
seniority: Record<'any' | 'junior' | 'mid' | 'senior', string>;
|
||||
@@ -248,6 +261,28 @@ const es: Copy = {
|
||||
links: 'Enlaces',
|
||||
since: 'En el colectivo desde',
|
||||
},
|
||||
radar: {
|
||||
eyebrow: 'Radar tecnológico',
|
||||
lead: 'Las herramientas con las que trabajamos y lo que opinamos de cada una: qué usamos sin dudar, qué estamos probando y qué preferimos evitar.',
|
||||
ringLead: {
|
||||
adopt: 'Lo usamos en proyectos reales y lo recomendamos sin reservas.',
|
||||
trial: 'Lo estamos probando en proyectos donde el riesgo es asumible.',
|
||||
assess: 'Nos interesa y lo estamos estudiando, sin usarlo aún en serio.',
|
||||
hold: 'Por ahora preferimos no empezar nada nuevo con esto.',
|
||||
},
|
||||
categories: {
|
||||
language: 'Lenguaje',
|
||||
framework: 'Framework',
|
||||
datastore: 'Datos',
|
||||
infrastructure: 'Infraestructura',
|
||||
tool: 'Herramienta',
|
||||
practice: 'Práctica',
|
||||
},
|
||||
website: 'Web oficial',
|
||||
projects: 'proyectos',
|
||||
empty: 'Todavía no hemos colocado ninguna tecnología en el radar.',
|
||||
seeAll: 'Ver el radar completo',
|
||||
},
|
||||
call: {
|
||||
eyebrow: 'Convocatoria',
|
||||
seniority: { any: 'Cualquier nivel', junior: 'Junior', mid: 'Intermedio', senior: 'Senior' },
|
||||
@@ -386,6 +421,28 @@ const en: Copy = {
|
||||
links: 'Links',
|
||||
since: 'In the collective since',
|
||||
},
|
||||
radar: {
|
||||
eyebrow: 'Tech radar',
|
||||
lead: 'The tools we work with and what we think of each: what we use without a second thought, what we are trying out and what we would rather avoid.',
|
||||
ringLead: {
|
||||
adopt: 'We use it on real projects and recommend it without reservation.',
|
||||
trial: 'We are trying it out on projects where the risk is acceptable.',
|
||||
assess: 'It interests us and we are looking into it, without using it seriously yet.',
|
||||
hold: 'For now we would rather not start anything new with it.',
|
||||
},
|
||||
categories: {
|
||||
language: 'Language',
|
||||
framework: 'Framework',
|
||||
datastore: 'Data',
|
||||
infrastructure: 'Infrastructure',
|
||||
tool: 'Tool',
|
||||
practice: 'Practice',
|
||||
},
|
||||
website: 'Official website',
|
||||
projects: 'projects',
|
||||
empty: 'We have not placed any technology on the radar yet.',
|
||||
seeAll: 'See the full radar',
|
||||
},
|
||||
call: {
|
||||
eyebrow: 'Open call',
|
||||
seniority: { any: 'Any level', junior: 'Junior', mid: 'Mid-level', senior: 'Senior' },
|
||||
@@ -524,6 +581,28 @@ const ca: Copy = {
|
||||
links: 'Enllaços',
|
||||
since: 'Al col·lectiu des de',
|
||||
},
|
||||
radar: {
|
||||
eyebrow: 'Radar tecnològic',
|
||||
lead: "Les eines amb què treballem i què en pensem de cadascuna: què fem servir sense dubtar, què estem provant i què preferim evitar.",
|
||||
ringLead: {
|
||||
adopt: 'El fem servir en projectes reals i el recomanem sense reserves.',
|
||||
trial: "L'estem provant en projectes on el risc és assumible.",
|
||||
assess: "Ens interessa i l'estem estudiant, sense fer-lo servir encara seriosament.",
|
||||
hold: 'Ara per ara preferim no començar res de nou amb això.',
|
||||
},
|
||||
categories: {
|
||||
language: 'Llenguatge',
|
||||
framework: 'Framework',
|
||||
datastore: 'Dades',
|
||||
infrastructure: 'Infraestructura',
|
||||
tool: 'Eina',
|
||||
practice: 'Pràctica',
|
||||
},
|
||||
website: 'Web oficial',
|
||||
projects: 'projectes',
|
||||
empty: 'Encara no hem col·locat cap tecnologia al radar.',
|
||||
seeAll: 'Veure el radar sencer',
|
||||
},
|
||||
call: {
|
||||
eyebrow: 'Convocatòria',
|
||||
seniority: { any: 'Qualsevol nivell', junior: 'Júnior', mid: 'Intermedi', senior: 'Sènior' },
|
||||
|
||||
@@ -13,7 +13,13 @@ import {
|
||||
type MaintenanceNotice,
|
||||
type SitePayload,
|
||||
} from './api';
|
||||
import type { PublicCall, PublicMember, PublicPage, PublicProject } from './api.types';
|
||||
import type {
|
||||
PublicCall,
|
||||
PublicMember,
|
||||
PublicPage,
|
||||
PublicProject,
|
||||
PublicTechnology,
|
||||
} from './api.types';
|
||||
import { resolveRoute, type RouteMatch } from './routes';
|
||||
|
||||
/** The record, tagged with its type so the template can switch on it. */
|
||||
@@ -21,7 +27,8 @@ export type RoutedRecord =
|
||||
| { type: 'page'; record: PublicPage }
|
||||
| { type: 'project'; record: PublicProject }
|
||||
| { type: 'member'; record: PublicMember }
|
||||
| { type: 'call'; record: PublicCall };
|
||||
| { type: 'call'; record: PublicCall }
|
||||
| { type: 'radar'; record: PublicTechnology[] };
|
||||
|
||||
export interface RoutedPageData {
|
||||
locale: Locale;
|
||||
@@ -74,6 +81,11 @@ async function fetchRecord(route: RouteMatch): Promise<RoutedRecord | null> {
|
||||
const { data } = await pireonApi.call(locale, slug);
|
||||
return data ? { type: 'call', record: data } : null;
|
||||
}
|
||||
case 'radar': {
|
||||
// An empty radar is still the radar: the page says so rather than 404.
|
||||
const { data, error } = await pireonApi.radar(locale);
|
||||
return error ? null : { type: 'radar', record: data ?? [] };
|
||||
}
|
||||
default:
|
||||
// A type the site has no page for yet: an address the API hands out
|
||||
// but nothing here renders, which is a 404 until it does.
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from './api';
|
||||
import { homeHref, resolveRoute, SECTIONS } from './routes';
|
||||
import {
|
||||
homeHref,
|
||||
listingAlternates,
|
||||
listingPath,
|
||||
resolveRoute,
|
||||
SECTIONS,
|
||||
} from './routes';
|
||||
|
||||
describe('resolveRoute', () => {
|
||||
it('reads a page at the root as the default language', () => {
|
||||
@@ -77,6 +83,33 @@ describe('resolveRoute', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('listings', () => {
|
||||
it('reads the radar in every language, before any page of that slug', () => {
|
||||
expect(resolveRoute('/radar')).toEqual({
|
||||
type: 'radar',
|
||||
locale: 'es',
|
||||
slug: '',
|
||||
});
|
||||
expect(resolveRoute('/ca/radar')).toEqual({
|
||||
type: 'radar',
|
||||
locale: 'ca',
|
||||
slug: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('builds the address resolveRoute reads back', () => {
|
||||
for (const locale of SUPPORTED_LOCALES) {
|
||||
expect(resolveRoute(listingPath('radar', locale))?.type).toBe('radar');
|
||||
}
|
||||
|
||||
expect(listingAlternates('radar')).toEqual({
|
||||
es: '/radar',
|
||||
en: '/en/radar',
|
||||
ca: '/ca/radar',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('homeHref', () => {
|
||||
it('answers on ?lang= for every language but the default', () => {
|
||||
expect(homeHref(DEFAULT_LOCALE)).toBe('/');
|
||||
|
||||
@@ -15,6 +15,9 @@ 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';
|
||||
|
||||
/**
|
||||
* The section each type lives under, per language. The API's own
|
||||
* `config/site.php` (`paths`) is the source; a change there has to be made here
|
||||
@@ -30,9 +33,18 @@ export const SECTIONS: Record<
|
||||
service: { es: 'servicios', en: 'services', ca: 'serveis' },
|
||||
};
|
||||
|
||||
/**
|
||||
* Where each listing lives, per language. A panel page with the same slug
|
||||
* would be shadowed by it, so these are few and deliberately named.
|
||||
*/
|
||||
export const LISTINGS: Record<ListingType, Record<Locale, string>> = {
|
||||
radar: { es: 'radar', en: 'radar', ca: 'radar' },
|
||||
};
|
||||
|
||||
export interface RouteMatch {
|
||||
type: RecordType;
|
||||
type: RecordType | ListingType;
|
||||
locale: Locale;
|
||||
/** Empty for a listing. */
|
||||
slug: string;
|
||||
}
|
||||
|
||||
@@ -51,7 +63,13 @@ export function resolveRoute(pathname: string): RouteMatch | null {
|
||||
const rest = prefixed ? segments.slice(1) : segments;
|
||||
|
||||
if (rest.length === 1) {
|
||||
return { type: 'page', locale, slug: rest[0] };
|
||||
const listing = (Object.keys(LISTINGS) as ListingType[]).find(
|
||||
(candidate) => LISTINGS[candidate][locale] === rest[0],
|
||||
);
|
||||
|
||||
return listing
|
||||
? { type: listing, locale, slug: '' }
|
||||
: { type: 'page', locale, slug: rest[0] };
|
||||
}
|
||||
|
||||
if (rest.length === 2) {
|
||||
@@ -65,6 +83,20 @@ export function resolveRoute(pathname: string): RouteMatch | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A listing's address in one language, as `resolveRoute` reads it back. */
|
||||
export function listingPath(listing: ListingType, locale: Locale): string {
|
||||
return `${locale === DEFAULT_LOCALE ? '' : `/${locale}`}/${LISTINGS[listing][locale]}`;
|
||||
}
|
||||
|
||||
/** The same listing in every language, for `hreflang`. */
|
||||
export function listingAlternates(
|
||||
listing: ListingType,
|
||||
): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
SUPPORTED_LOCALES.map((locale) => [locale, listingPath(listing, locale)]),
|
||||
);
|
||||
}
|
||||
|
||||
/** The landing itself in a given language. It answers on `?lang=`, not a prefix. */
|
||||
export function homeHref(locale: Locale): string {
|
||||
return locale === DEFAULT_LOCALE ? '/' : `/?lang=${locale}`;
|
||||
|
||||
Reference in New Issue
Block a user