From 40829cd1aeedca0b50d54abab0bd534823e2b708 Mon Sep 17 00:00:00 2001 From: Amr Mohammed El-Sheraey <141947355+AmrMsCLL@users.noreply.github.com> Date: Sat, 7 Nov 2026 03:33:17 +0200 Subject: [PATCH 1/4] fix(status): count only the running fleet in the hourly uptime summary The summary counted every project with an uptime row in the last hour, so a retired project kept its 30 days of rows in the count and an unlisted host nobody sees made the console read 6 of 7 up beside a fleet of 4 of 4. The pinger now checks the listed, live, deployed projects that /status shows as running, and the summary counts that fleet only. --- site/app/components/EpicCard.tsx | 54 ------------------------ site/app/components/ProjectCard.tsx | 59 -------------------------- site/server/uptime.test.ts | 65 +++++++++++++++++++++++++++++ site/server/uptime.ts | 8 +++- 4 files changed, 71 insertions(+), 115 deletions(-) delete mode 100644 site/app/components/EpicCard.tsx delete mode 100644 site/app/components/ProjectCard.tsx create mode 100644 site/server/uptime.test.ts diff --git a/site/app/components/EpicCard.tsx b/site/app/components/EpicCard.tsx deleted file mode 100644 index 4749eca..0000000 --- a/site/app/components/EpicCard.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Link } from 'react-router'; -import type { Project } from '../types'; -import { accentStyle } from './kit/basics'; -import ProjectTile from './ProjectTile'; -import Status from './Status'; -import TagList from './TagList'; - -interface Props { - epic: Project; - projects: Project[]; - context?: string; - accent?: string; -} - -export default function EpicCard({ epic, projects, context, accent }: Props) { - return ( -
-
-
-

- epic · {projects.length} {projects.length === 1 ? 'project' : 'projects'} -

-

- - {epic.name} - -

-
- -
- -

{epic.summary}

- - {context && ( -

- {context} -

- )} - - - - {projects.length > 0 && ( -
- {projects.map((project) => ( - - ))} -
- )} -
- ); -} diff --git a/site/app/components/ProjectCard.tsx b/site/app/components/ProjectCard.tsx deleted file mode 100644 index ae3578f..0000000 --- a/site/app/components/ProjectCard.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Link } from 'react-router'; -import AuthorLink from './AuthorLink'; -import Status from './Status'; -import TagList from './TagList'; -import type { Project } from '../types'; - -interface Props { - project: Project; - context?: string; -} - -export default function ProjectCard({ project, context }: Props) { - return ( -
-
-

- - {project.name} - -

- -
- -

{project.summary}

- - {context && ( -

- {context} -

- )} - - - -
- - {project.authors.map((author, index) => ( - - {index > 0 && ·} - - - ))} - - - {project.url && ( - - visit ↗ - - )} - - source ↗ - - -
-
- ); -} diff --git a/site/server/uptime.test.ts b/site/server/uptime.test.ts new file mode 100644 index 0000000..4cff558 --- /dev/null +++ b/site/server/uptime.test.ts @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { applyEntries, type RegistryEntry } from './registry.ts'; +import { memoryDatabase, recordingBus } from './testing.ts'; +import { liveHosts, recordUptimeSummary } from './uptime.ts'; + +const entry = (slug: string, overrides: Partial): RegistryEntry => ({ + slug, + kind: 'project', + name: slug, + summary: 'A project summary long enough to pass.', + repo: `project-graphite/${slug}`, + owner: 'amrmscll', + authors: ['AmrMsCLL'], + status: 'live', + deploy: { kind: 'compose', host: `${slug}.project-graphite.com` }, + ...overrides, +}); + +function fleet() { + const db = memoryDatabase(); + applyEntries( + db, + [ + entry('strata', {}), + entry('traceboard', { listed: false }), + entry('message-lab', { status: 'archived', deploy: { kind: 'none' } }), + entry('graphite-ui', { status: 'wip', deploy: { kind: 'none' } }), + ], + '2026-11-07T00:00:00.000Z', + ); + return db; +} + +describe('liveHosts', () => { + it('pings only the listed, live, deployed projects the site shows as running', () => { + assert.deepEqual(liveHosts(fleet()), [ + { project: 'strata', host: 'strata.project-graphite.com' }, + ]); + }); +}); + +describe('recordUptimeSummary', () => { + it('counts the running fleet only, not retired or unlisted projects with old checks', async () => { + const db = fleet(); + const minute = new Date().toISOString().slice(0, 16); + const insert = db.prepare('INSERT INTO uptime (project, minute, ok, ms) VALUES (?, ?, ?, ?)'); + insert.run('strata', minute, 1, 120); + insert.run('traceboard', minute, 0, null); + insert.run('message-lab', minute, 1, 90); + const { bus, recorded } = recordingBus(); + + await recordUptimeSummary(db, bus); + + assert.equal(recorded.length, 1); + assert.equal(recorded[0].summary, '1 of 1 up over the last hour'); + assert.deepEqual(recorded[0].detail, { up: 1, total: 1, p95Ms: 120 }); + }); + + it('records nothing while the fleet has no checks yet', async () => { + const { bus, recorded } = recordingBus(); + await recordUptimeSummary(fleet(), bus); + assert.equal(recorded.length, 0); + }); +}); diff --git a/site/server/uptime.ts b/site/server/uptime.ts index e055d06..8376b88 100644 --- a/site/server/uptime.ts +++ b/site/server/uptime.ts @@ -4,7 +4,7 @@ import { storedEntries } from './registry.ts'; export function liveHosts(db: Database) { return storedEntries(db).flatMap((entry) => - entry.status === 'live' && entry.deploy.kind !== 'none' + entry.status === 'live' && entry.listed !== false && entry.deploy.kind !== 'none' ? [{ project: entry.slug, host: entry.deploy.host }] : [], ); @@ -57,7 +57,11 @@ export function percentile(values: number[], fraction: number) { export async function recordUptimeSummary(db: Database, bus: Bus) { const since = new Date(Date.now() - 3_600_000).toISOString().slice(0, 16); - const rows = db.prepare('SELECT project, ok, ms FROM uptime WHERE minute >= ?').all(since); + const rows = db + .prepare( + 'SELECT project, ok, ms FROM uptime WHERE minute >= ? AND project IN (SELECT value FROM json_each(?))', + ) + .all(since, JSON.stringify(liveHosts(db).map((entry) => entry.project))); if (rows.length === 0) { return; } From b1f7101be6795d34f8bd0c2bd45f53d2785d6241 Mon Sep 17 00:00:00 2001 From: Amr Mohammed El-Sheraey <141947355+AmrMsCLL@users.noreply.github.com> Date: Sat, 7 Nov 2026 03:33:18 +0200 Subject: [PATCH 2/4] fix(errors): answer client errors with their status instead of recording them A thrown 4xx response, a route error response or an error carrying a 4xx status (a denied dotfile, a missing upload, a method the route does not accept) was captured as a site error, alerted the owner and answered 500. They answer with their own status now and are never captured; a failed response is recorded as HTTP rather than [object Object]. --- site/server/app.ts | 17 +++++++++++------ site/server/errors.test.ts | 20 +++++++++++++++++++- site/server/errors.ts | 27 ++++++++++++++++++++++++--- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/site/server/app.ts b/site/server/app.ts index a1e9258..dbc90b8 100644 --- a/site/server/app.ts +++ b/site/server/app.ts @@ -8,7 +8,7 @@ import type { Bus } from './bus.ts'; import { config } from './config.ts'; import type { Database } from './db.ts'; import { recordDeploy, validReport } from './deploys.ts'; -import type { Capture } from './errors.ts'; +import { clientError, describe, httpStatus, type Capture } from './errors.ts'; import { validSignature } from './github.ts'; import { contentSecurityPolicy, securityHeaders } from './headers.ts'; import { createVerifier } from './internal.ts'; @@ -307,12 +307,17 @@ export function createApp( app.use((error: unknown, request: Request, response: Response, _next: NextFunction) => { capture(error, { route: request.path, requestId: response.locals.requestId }); - if (!response.headersSent) { - response - .status(500) - .type('text') - .send(`Something went wrong. Request ${response.locals.requestId}.`); + if (response.headersSent) { + return; + } + if (clientError(error)) { + response.status(httpStatus(error)!).type('text').send(describe(error).message); + return; } + response + .status(500) + .type('text') + .send(`Something went wrong. Request ${response.locals.requestId}.`); }); return app; diff --git a/site/server/errors.test.ts b/site/server/errors.test.ts index 7f22e35..c9de44f 100644 --- a/site/server/errors.test.ts +++ b/site/server/errors.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { createErrors, fingerprint } from './errors.ts'; +import { createErrors, describe as describeError, fingerprint } from './errors.ts'; import { memoryDatabase, recordingBus } from './testing.ts'; const stack = (line: number) => @@ -71,4 +71,22 @@ describe('capture', () => { await settle(); assert.equal(alerts.length, 2); }); + + it('ignores client errors: thrown 4xx responses and route errors with a status', async () => { + const { db, alerts, capture } = await setup(); + capture(new Response(null, { status: 404 }), { route: '/nope' }); + capture({ status: 405, statusText: 'Method Not Allowed', data: null }, { route: '/' }); + capture(Object.assign(new Error('Forbidden'), { status: 403 }), { route: '/uploads/.env' }); + await settle(); + assert.equal(db.prepare('SELECT count(*) AS count FROM errors').get()!.count, 0); + assert.equal(alerts.length, 0); + }); + + it('records a failed response under its status', async () => { + const { db, capture } = await setup(); + capture(new Response(null, { status: 503 }), { route: '/live' }); + await settle(); + assert.equal(db.prepare('SELECT message FROM errors').get()!.message, 'HTTP 503'); + assert.equal(describeError({ status: 500 }).message, 'HTTP 500'); + }); }); diff --git a/site/server/errors.ts b/site/server/errors.ts index 921c831..1faabbe 100644 --- a/site/server/errors.ts +++ b/site/server/errors.ts @@ -10,10 +10,28 @@ export interface Where { const DAY_MS = 86_400_000; +export function httpStatus(error: unknown) { + if (error instanceof Response) { + return error.status; + } + if (typeof error === 'object' && error !== null) { + const { status, statusCode } = error as { status?: unknown; statusCode?: unknown }; + return [status, statusCode].find((value) => typeof value === 'number') as number | undefined; + } + return undefined; +} + +export function clientError(error: unknown) { + const status = httpStatus(error); + return status !== undefined && status >= 400 && status < 500; +} + export function describe(error: unknown) { - return error instanceof Error - ? { message: error.message, stack: error.stack ?? null } - : { message: String(error), stack: null }; + if (error instanceof Error) { + return { message: error.message, stack: error.stack ?? null }; + } + const status = httpStatus(error); + return { message: status === undefined ? String(error) : `HTTP ${status}`, stack: null }; } export function fingerprint(message: string, stack: string | null) { @@ -70,6 +88,9 @@ export function createErrors( } return function capture(error: unknown, where: Where = {}) { + if (clientError(error)) { + return; + } const { message, stack } = describe(error); const key = fingerprint(message, stack); const now = new Date().toISOString(); From a0b0bedaa31bd8e627c9da97848b645bc10ff8bd Mon Sep 17 00:00:00 2001 From: Amr Mohammed El-Sheraey <141947355+AmrMsCLL@users.noreply.github.com> Date: Sat, 7 Nov 2026 03:33:18 +0200 Subject: [PATCH 3/4] fix(copy): pluralise count labels, name a project's real status, hide empty facts One plural() behind every count label, so a page no longer reads 1 releases. The window bar and the share label name an archived or in-progress project's status instead of calling it source only. The home share card counts root projects like the home counter; /subscribe, /search and /privacy get share cards. The rail stops saying no deploy recorded yet for projects that never report one, /subscribe hides zero facts, the empty wall uses the shared empty state, and the palette and message fields have accessible names. --- site/app/components/kit/Palette.tsx | 1 + site/app/components/kit/live.tsx | 4 ++-- site/app/components/kit/map.tsx | 5 +++-- site/app/components/kit/panels.tsx | 4 +--- site/app/components/kit/projects.tsx | 5 ++++- site/app/format.ts | 4 ++++ site/app/pages/member-page.tsx | 12 ++++++------ site/app/pages/project-page.tsx | 8 ++++---- site/app/routes/admin.tsx | 13 ++++++++++--- site/app/routes/author.tsx | 3 ++- site/app/routes/blog-mine.tsx | 4 ++-- site/app/routes/blog.tsx | 5 +++-- site/app/routes/changelog.tsx | 8 ++++---- site/app/routes/epics.tsx | 12 ++++++------ site/app/routes/member-changelog.tsx | 5 +++-- site/app/routes/members.tsx | 18 +++++++++++------- site/app/routes/messages.tsx | 24 +++++++++++++++++------- site/app/routes/post.tsx | 4 ++-- site/app/routes/project-changelog.tsx | 4 ++-- site/app/routes/projects.tsx | 15 ++++++--------- site/app/routes/search.tsx | 15 +++++++++------ site/app/routes/subscribe.tsx | 9 ++++++--- site/app/share.ts | 10 +++++++--- 23 files changed, 115 insertions(+), 77 deletions(-) diff --git a/site/app/components/kit/Palette.tsx b/site/app/components/kit/Palette.tsx index b44bbf2..8484878 100644 --- a/site/app/components/kit/Palette.tsx +++ b/site/app/components/kit/Palette.tsx @@ -73,6 +73,7 @@ export function Palette() { { setQuery(event.target.value); diff --git a/site/app/components/kit/live.tsx b/site/app/components/kit/live.tsx index 439d14a..e13eff6 100644 --- a/site/app/components/kit/live.tsx +++ b/site/app/components/kit/live.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'; import { Link } from 'react-router'; import { useFolded } from '../../folds'; -import { clock, day, describe } from '../../format'; +import { clock, day, describe, plural } from '../../format'; import { useRoot } from '../../hub'; import { useConnection, @@ -73,7 +73,7 @@ export function Views({ kind, id }: { kind: 'post' | 'project'; id: string }) { const shown = useCountUp(count); return count > 0 ? ( - {shown} {count === 1 ? 'view' : 'views'} + {shown} {plural(count, 'view')} ) : null; } diff --git a/site/app/components/kit/map.tsx b/site/app/components/kit/map.tsx index 6a13dd8..e07f5a8 100644 --- a/site/app/components/kit/map.tsx +++ b/site/app/components/kit/map.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { plural } from '../../format'; import { useLiveMessages } from '../../live'; import type { Country } from '../../types'; import Status from '../Status'; @@ -40,8 +41,8 @@ export function VisitorMap({ initial }: { initial: Country[] }) {
- {tabs} {tabs === 1 ? 'tab' : 'tabs'} from {countries.length}{' '} - {countries.length === 1 ? 'country' : 'countries'} + {tabs} {plural(tabs, 'tab')} from {countries.length}{' '} + {plural(countries.length, 'country', 'countries')} last 10 minutes
diff --git a/site/app/components/kit/panels.tsx b/site/app/components/kit/panels.tsx index ae3edbc..4be0890 100644 --- a/site/app/components/kit/panels.tsx +++ b/site/app/components/kit/panels.tsx @@ -43,9 +43,7 @@ export function RunningPanel({ running }: { running: Running[] }) { - {project.deploy - ? `sha-${shortTag(project.deploy.tag)} · ${day(project.deploy.at)}` - : 'no deploy recorded yet'} + {project.deploy && `sha-${shortTag(project.deploy.tag)} · ${day(project.deploy.at)}`} {project.week.map((state, index) => ( - {project.url ? new URL(project.url).host : `${project.slug} · source only`} + {project.url + ? new URL(project.url).host + : `${project.slug} · ${STATUS_LABEL[project.status]}`} ); diff --git a/site/app/format.ts b/site/app/format.ts index b33fc20..807405e 100644 --- a/site/app/format.ts +++ b/site/app/format.ts @@ -9,6 +9,10 @@ export function day(iso: string) { return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`; } +export function plural(count: number, noun: string, many = `${noun}s`) { + return count === 1 ? noun : many; +} + export function shortTag(tag: unknown) { return typeof tag === 'string' ? tag.replace(/^sha-/, '').slice(0, 7) : ''; } diff --git a/site/app/pages/member-page.tsx b/site/app/pages/member-page.tsx index 494298d..e7f20f7 100644 --- a/site/app/pages/member-page.tsx +++ b/site/app/pages/member-page.tsx @@ -7,6 +7,7 @@ import { ChangelogPanel, RunningPanel } from '../components/kit/panels'; import { SubscribePanel } from '../components/kit/subscribe'; import { Page } from '../components/Shell'; import type { Hub } from '../context'; +import { plural } from '../format'; import { useHub, useRoot } from '../hub'; import { createModel } from '../model'; import type { Member } from '../types'; @@ -38,6 +39,8 @@ export function MemberHero({ member, actions }: { member: Member; actions?: Reac const hub = useHub(); const live = hub.projectsOwnedBy(member).filter((project) => project.status === 'live').length; const posts = hub.postsBy(member).length; + const projects = hub.projectsForMember(member).length + hub.epicsForMember(member).length; + const releases = hub.releasesOwnedBy(member).length; return ( } @@ -48,15 +51,12 @@ export function MemberHero({ member, actions }: { member: Member; actions?: Reac actions={actions} facts={ <> - + {live > 0 && } - + {posts > 0 && ( - + )} {member.joined && } diff --git a/site/app/pages/project-page.tsx b/site/app/pages/project-page.tsx index 481038f..31b97c1 100644 --- a/site/app/pages/project-page.tsx +++ b/site/app/pages/project-page.tsx @@ -14,7 +14,7 @@ import { Page } from '../components/Shell'; import Status from '../components/Status'; import TagList from '../components/TagList'; import type { Hub } from '../context'; -import { day, shortTag } from '../format'; +import { day, plural, shortTag } from '../format'; import { useHub, useRoot } from '../hub'; import { useView, useViews } from '../live'; import { changelogPath } from '../model'; @@ -148,9 +148,9 @@ export function ProjectPage({ data: loaderData }: { data: ReturnType {releases[0] && } - + {releases[0] && } - {views > 0 && } + {views > 0 && } } > @@ -183,7 +183,7 @@ export function ProjectPage({ data: loaderData }: { data: ReturnType - {writing.length} {writing.length === 1 ? 'post' : 'posts'} + {writing.length} {plural(writing.length, 'post')} } > diff --git a/site/app/routes/admin.tsx b/site/app/routes/admin.tsx index 4478d82..1258ca7 100644 --- a/site/app/routes/admin.tsx +++ b/site/app/routes/admin.tsx @@ -6,6 +6,7 @@ import { Confirm, Segmented, Sortable, Switch } from '../components/kit/controls import { Fact, PageHeader } from '../components/kit/page'; import { Page } from '../components/Shell'; import { hubContext, type Hub } from '../context'; +import { plural } from '../format'; import { useRoot } from '../hub'; import { useSpotlight } from '../motion/motion'; import { @@ -668,9 +669,15 @@ export default function AdminPage({ loaderData }: Route.ComponentProps) { facts={ <> - - - + + + } > diff --git a/site/app/routes/author.tsx b/site/app/routes/author.tsx index 0378305..bdaf3cb 100644 --- a/site/app/routes/author.tsx +++ b/site/app/routes/author.tsx @@ -6,6 +6,7 @@ import { ChangelogPanel } from '../components/kit/panels'; import { PostCard } from '../components/kit/posts'; import { Page } from '../components/Shell'; import { hubContext } from '../context'; +import { plural } from '../format'; import { useHub, useRoot } from '../hub'; import { authorFrom, viewerHandle } from '../pages/blog'; import { topicsFor } from '../topics'; @@ -102,7 +103,7 @@ export default function Author({ loaderData }: Route.ComponentProps) { } facts={ <> - + {words > 0 && } {posts[0] && } diff --git a/site/app/routes/blog-mine.tsx b/site/app/routes/blog-mine.tsx index 4352318..2573052 100644 --- a/site/app/routes/blog-mine.tsx +++ b/site/app/routes/blog-mine.tsx @@ -6,7 +6,7 @@ import { Confirm, Segmented } from '../components/kit/controls'; import { Fact, PageHeader } from '../components/kit/page'; import { Page } from '../components/Shell'; import { hubContext } from '../context'; -import { day } from '../format'; +import { day, plural } from '../format'; import { useHub, useRoot } from '../hub'; import { checkedForm, EDITOR_PATH, member, viewerHandle } from '../pages/blog'; import { postPath, STATUS_LABEL, type PostStatus } from '../posts'; @@ -106,7 +106,7 @@ export default function BlogMine({ loaderData }: Route.ComponentProps) { } facts={ <> - + diff --git a/site/app/routes/blog.tsx b/site/app/routes/blog.tsx index aa148fa..205ff44 100644 --- a/site/app/routes/blog.tsx +++ b/site/app/routes/blog.tsx @@ -10,6 +10,7 @@ import { PostCard, WritingStrip } from '../components/kit/posts'; import { SubscribePanel } from '../components/kit/subscribe'; import { Page } from '../components/Shell'; import { hubContext } from '../context'; +import { plural } from '../format'; import { useHub, useRoot } from '../hub'; import { viewerHandle } from '../pages/blog'; import { topicsFor } from '../topics'; @@ -132,8 +133,8 @@ export default function Blog({ loaderData }: Route.ComponentProps) { } facts={ <> - - + + {month > 0 && } } diff --git a/site/app/routes/changelog.tsx b/site/app/routes/changelog.tsx index 14dfa46..095e9f2 100644 --- a/site/app/routes/changelog.tsx +++ b/site/app/routes/changelog.tsx @@ -3,7 +3,7 @@ import { Panel } from '../components/kit/basics'; import { Fact, PageHeader } from '../components/kit/page'; import { Page } from '../components/Shell'; import Timeline from '../components/Timeline'; -import { day } from '../format'; +import { day, plural } from '../format'; import { useHub } from '../hub'; import { topicsFor } from '../topics'; @@ -24,9 +24,9 @@ export default function Changelog() { title="Changelog" facts={ <> - - - + + + {releases[0] && } } diff --git a/site/app/routes/epics.tsx b/site/app/routes/epics.tsx index 4c99c2d..0ddca11 100644 --- a/site/app/routes/epics.tsx +++ b/site/app/routes/epics.tsx @@ -8,7 +8,7 @@ import Status from '../components/Status'; import TagList from '../components/TagList'; import { Page } from '../components/Shell'; import { hubContext } from '../context'; -import { day } from '../format'; +import { day, plural } from '../format'; import { useHub } from '../hub'; import { useSpotlight } from '../motion/motion'; import { topicsFor } from '../topics'; @@ -44,8 +44,8 @@ function Epic({ epic, today }: { epic: Project; today: string }) {

{epic.summary}

- - + + {latest && } @@ -108,9 +108,9 @@ export default function Epics({ loaderData }: Route.ComponentProps) { title="Epics" facts={ <> - - - + + + } /> diff --git a/site/app/routes/member-changelog.tsx b/site/app/routes/member-changelog.tsx index 89e5184..20b265c 100644 --- a/site/app/routes/member-changelog.tsx +++ b/site/app/routes/member-changelog.tsx @@ -5,6 +5,7 @@ import { Page } from '../components/Shell'; import { Link } from 'react-router'; import type { Route } from './+types/member-changelog'; import Timeline from '../components/Timeline'; +import { plural } from '../format'; import { found, modelFrom, useHub } from '../hub'; import { changelogPath } from '../model'; import { topicsFor } from '../topics'; @@ -38,8 +39,8 @@ export default function MemberChangelog({ loaderData }: Route.ComponentProps) { actions={} facts={ <> - - + + {releases[0] && } } diff --git a/site/app/routes/members.tsx b/site/app/routes/members.tsx index 18f6938..4dd0523 100644 --- a/site/app/routes/members.tsx +++ b/site/app/routes/members.tsx @@ -6,6 +6,7 @@ import { Fact, loadRail, PageHeader, StandardRail } from '../components/kit/page import { Page } from '../components/Shell'; import TagList from '../components/TagList'; import { hubContext } from '../context'; +import { plural } from '../format'; import { useHub } from '../hub'; import { useOnline } from '../live'; import { useSpotlight, useTilt } from '../motion/motion'; @@ -61,14 +62,14 @@ function Card({ {member.bio &&

{member.bio}

}
- {projects} {projects === 1 ? 'project' : 'projects'} + {projects} {plural(projects, 'project')} - {releases} {releases === 1 ? 'release' : 'releases'} + {releases} {plural(releases, 'release')} {member.followers !== null && ( - {member.followers} followers + {member.followers} {plural(member.followers, 'follower')} )} {member.joined && ( @@ -112,10 +113,13 @@ export default function Members({ loaderData }: Route.ComponentProps) { title="Members" facts={ <> - - - - + + + + } /> diff --git a/site/app/routes/messages.tsx b/site/app/routes/messages.tsx index 0184b05..158cacf 100644 --- a/site/app/routes/messages.tsx +++ b/site/app/routes/messages.tsx @@ -100,13 +100,20 @@ function Leave() { ) : ( - +