diff --git a/apps/website/scripts/check-dist.ts b/apps/website/scripts/check-dist.ts index 497fb70..8f4d0a0 100644 --- a/apps/website/scripts/check-dist.ts +++ b/apps/website/scripts/check-dist.ts @@ -68,6 +68,9 @@ export function findLiterals(srcFiles: { path: string; text: string }[]): string ); } +/** Files whose text is code or content; images and fonts are skipped by the literal check. */ +export const isSourceText = (path: string) => /\.(astro|ts|tsx|js|mjs|css|md|mdx|json)$/i.test(path); + /** Every rule list that must read the same on the page and in brand.md. */ export function parityRules(b: Brand): string[] { return [ @@ -118,7 +121,9 @@ function main(): void { const brandHtml = read('brand/index.html'); const md = read('brand.md'); const redirects = existsSync(join(dist, '_redirects')) ? parseRedirects(read('_redirects')) : []; - const src = walk(join(app, 'src')).map((p) => ({ path: relative(app, p), text: readFileSync(p, 'utf8') })); + const src = walk(join(app, 'src')) + .filter(isSourceText) + .map((p) => ({ path: relative(app, p), text: readFileSync(p, 'utf8') })); const errors = [ ...required.filter((f) => !existsSync(join(dist, f))).map((f) => `dist/${f} was not built`), diff --git a/apps/website/src/assets/organizers/cyrus-hiyas.jpg b/apps/website/src/assets/organizers/cyrus-hiyas.jpg new file mode 100644 index 0000000..980ecd5 Binary files /dev/null and b/apps/website/src/assets/organizers/cyrus-hiyas.jpg differ diff --git a/apps/website/src/assets/organizers/dorell-james.jpg b/apps/website/src/assets/organizers/dorell-james.jpg new file mode 100644 index 0000000..8e6056b Binary files /dev/null and b/apps/website/src/assets/organizers/dorell-james.jpg differ diff --git a/apps/website/src/assets/organizers/harlequin-doyon.jpg b/apps/website/src/assets/organizers/harlequin-doyon.jpg new file mode 100644 index 0000000..00a4e8e Binary files /dev/null and b/apps/website/src/assets/organizers/harlequin-doyon.jpg differ diff --git a/apps/website/src/assets/organizers/sieg-balona.jpg b/apps/website/src/assets/organizers/sieg-balona.jpg new file mode 100644 index 0000000..2a1087e Binary files /dev/null and b/apps/website/src/assets/organizers/sieg-balona.jpg differ diff --git a/apps/website/src/content/organizers.json b/apps/website/src/content/organizers.json index 0b76a49..2de1770 100644 --- a/apps/website/src/content/organizers.json +++ b/apps/website/src/content/organizers.json @@ -1,6 +1,6 @@ [ - { "id": "cyrus-hiyas", "name": "Cyrus Hiyas", "role": "Lead", "community": "Angular Cebu", "order": 1 }, - { "id": "dorell-james", "name": "Dorell James", "role": "Lead", "community": "React Cebu", "order": 2 }, - { "id": "harlequin-doyon", "name": "Harlequin Doyon", "role": "Lead", "community": "Vue.js Cebu", "order": 3 }, - { "id": "sieg-balona", "name": "Sieg Balona", "role": "Co-Lead", "community": "React Cebu", "order": 4 } + {"id": "cyrus-hiyas", "name": "Cyrus Hiyas", "role": "Lead", "community": "Angular Cebu", "url": "https://www.sleepnotfound.com/", "photo": "../assets/organizers/cyrus-hiyas.jpg", "order": 1}, + {"id": "dorell-james", "name": "Dorell James", "role": "Lead", "community": "React Cebu", "url": "https://www.dorelljames.com", "photo": "../assets/organizers/dorell-james.jpg", "order": 2}, + {"id": "harlequin-doyon", "name": "Harlequin Doyon", "role": "Lead", "community": "Vue.js Cebu", "url": "https://harlekoy.dev/", "photo": "../assets/organizers/harlequin-doyon.jpg", "order": 3}, + {"id": "sieg-balona", "name": "Sieg Balona", "role": "Co-Lead", "community": "React Cebu", "url": "https://siegb.dev/", "photo": "../assets/organizers/sieg-balona.jpg", "order": 4} ] diff --git a/apps/website/src/content/schemas.ts b/apps/website/src/content/schemas.ts index 30a3a83..21dabbc 100644 --- a/apps/website/src/content/schemas.ts +++ b/apps/website/src/content/schemas.ts @@ -14,13 +14,18 @@ export const projectSchema = ({ image }: Pick) => order: z.number(), }); -export const organizerSchema = z.object({ - id: z.string(), - name: z.string(), - role: z.string(), - community: z.string().optional(), - order: z.number(), -}); +export const organizerSchema = ({ image }: Pick) => + z.object({ + id: z.string(), + name: z.string(), + role: z.string(), + community: z.string().optional(), + /** Their own site. */ + url: z.string().url().optional(), + /** A real photo (or the avatar they publish), square, relative to organizers.json. */ + photo: image().optional(), + order: z.number(), + }); export const linkSchema = z.object({ id: z.string(), diff --git a/apps/website/src/pages/about.astro b/apps/website/src/pages/about.astro index 898cff1..a94092d 100644 --- a/apps/website/src/pages/about.astro +++ b/apps/website/src/pages/about.astro @@ -23,10 +23,13 @@ const organizers = (await getCollection('organizers')).sort((a, b) => a.data.ord { organizers.map(({ data }, i) => (
  • - {data.name} - - {data.role} - {data.community && ` · ${data.community}`} + {data.photo && } + + {data.url ? {`${data.name}\u00a0↗`} : {data.name}} + + {data.role} + {data.community && ` · ${data.community}`} +
  • )) @@ -82,15 +85,32 @@ const organizers = (await getCollection('organizers')).sort((a, b) => a.data.ord list-style: none; } .organizers li { - display: grid; - gap: 4px; - padding: 18px; + display: flex; + align-items: center; + gap: 14px; + padding: 16px; border: 1px solid var(--jc-line); border-radius: 10px; } + .face { + flex: none; + width: 64px; + height: 64px; + border-radius: 50%; + } + .who { + display: grid; + gap: 4px; + min-width: 0; + } .name { font-size: 18px; font-weight: 700; + color: var(--jc-text); + text-decoration: none; + } + a.name:hover { + color: var(--jc-accent); } .role { font-size: 13px; diff --git a/apps/website/test/check-dist.test.ts b/apps/website/test/check-dist.test.ts index 21e470a..7586142 100644 --- a/apps/website/test/check-dist.test.ts +++ b/apps/website/test/check-dist.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { brand } from '@jscebu/brand'; -import { findCursorViolations, findLiterals, findMissingLinks, findParityGaps, parityRules } from '../scripts/check-dist.ts'; +import { findCursorViolations, findLiterals, findMissingLinks, findParityGaps, isSourceText, parityRules } from '../scripts/check-dist.ts'; const cursor = ''; @@ -76,3 +76,10 @@ describe('parityRules', () => { expect(rules).toHaveLength(expected.length); }); }); + +describe('isSourceText', () => { + it('scans source files for literals but not images', () => { + expect(['a.astro', 'b.ts', 'c.css', 'd.mjs', 'e.md', 'f.json'].map(isSourceText)).toEqual([true, true, true, true, true, true]); + expect(['g.jpg', 'h.png', 'i.webp', 'j.svg', 'k.woff2'].map(isSourceText)).toEqual([false, false, false, false, false]); + }); +}); diff --git a/apps/website/test/content.test.ts b/apps/website/test/content.test.ts index d08b4f4..9c4cf02 100644 --- a/apps/website/test/content.test.ts +++ b/apps/website/test/content.test.ts @@ -1,4 +1,4 @@ -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { z } from 'astro/zod'; import { parse } from 'yaml'; import { describe, expect, it } from 'vitest'; @@ -10,7 +10,7 @@ const frontmatter = (md: string) => parse(md.match(/^---\n([\s\S]*?)\n---/)![1]) describe('site content', () => { it('lists the four organizers from /members, in order', () => { - const organizers = z.array(organizerSchema).parse(JSON.parse(read('organizers.json'))); + const organizers = z.array(organizerSchema({ image: () => z.string() })).parse(JSON.parse(read('organizers.json'))); expect(organizers.map((o) => [o.name, o.role])).toEqual([ ['Cyrus Hiyas', 'Lead'], ['Dorell James', 'Lead'], @@ -19,6 +19,21 @@ describe('site content', () => { ]); }); + it('links each organizer to their own site', () => { + const organizers = z.array(organizerSchema({ image: () => z.string() })).parse(JSON.parse(read('organizers.json'))); + expect(Object.fromEntries(organizers.map((o) => [o.name, o.url]))).toEqual({ + 'Cyrus Hiyas': 'https://www.sleepnotfound.com/', + 'Dorell James': 'https://www.dorelljames.com', + 'Harlequin Doyon': 'https://harlekoy.dev/', + 'Sieg Balona': 'https://siegb.dev/', + }); + }); + + it('gives each organizer a photo that exists in the repo', () => { + const organizers = z.array(organizerSchema({ image: () => z.string() })).parse(JSON.parse(read('organizers.json'))); + for (const o of organizers) expect(existsSync(new URL(o.photo!, new URL('organizers.json', content))), o.name).toBe(true); + }); + it('lists the official channels from /official-links', () => { const links = z.array(linkSchema).parse(JSON.parse(read('links.json'))); expect(links.map((l) => l.url)).toEqual(