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/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/members.tsx b/site/app/components/kit/members.tsx
index 81f4c16..ba6e0a2 100644
--- a/site/app/components/kit/members.tsx
+++ b/site/app/components/kit/members.tsx
@@ -1,7 +1,8 @@
import type { CSSProperties } from 'react';
import { Link } from 'react-router';
+import { plural } from '../../format';
import { useOnline } from '../../live';
-import { useSpotlight } from '../../motion/motion';
+import { useMotion, useSpotlight } from '../../motion/motion';
import type { Member } from '../../types';
import { accentStyle, Avatar } from './basics';
@@ -22,7 +23,7 @@ function MemberRow({
{member.name}
{member.headline ?? `@${member.github}`}
-
+
{projects}
@@ -61,8 +62,9 @@ export function Orbit({
label: string;
}) {
const online = useOnline();
+ const spinning = useMotion('orbit');
return (
-
+
{members.map((member, index) => (
);
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() {
) : (
-
+
)}
-
- {wall.length === 0 && nothing on the wall yet
}
- {wall.map((message) => (
-
- ))}
-
+ {wall.length === 0 ? (
+ nothing on the wall yet
+ ) : (
+
+ {wall.map((message) => (
+
+ ))}
+
+ )}
);
}
diff --git a/site/app/routes/post.tsx b/site/app/routes/post.tsx
index 35777e2..dee833d 100644
--- a/site/app/routes/post.tsx
+++ b/site/app/routes/post.tsx
@@ -9,7 +9,7 @@ import { SubscribePanel } from '../components/kit/subscribe';
import { Page } from '../components/Shell';
import TagList from '../components/TagList';
import { hubContext } from '../context';
-import { day } from '../format';
+import { day, plural } from '../format';
import { useHub, useRoot } from '../hub';
import { useView, useViews } from '../live';
import { createModel } from '../model';
@@ -203,7 +203,7 @@ export default function Post({ loaderData }: Route.ComponentProps) {
/>
)}
- {views > 0 && }
+ {views > 0 && }
>
}
>
diff --git a/site/app/routes/project-changelog.tsx b/site/app/routes/project-changelog.tsx
index 4d972ff..cfdabf5 100644
--- a/site/app/routes/project-changelog.tsx
+++ b/site/app/routes/project-changelog.tsx
@@ -7,7 +7,7 @@ import type { Route } from './+types/project-changelog';
import Status from '../components/Status';
import TagList from '../components/TagList';
import Timeline from '../components/Timeline';
-import { day } from '../format';
+import { day, plural } from '../format';
import { found, modelFrom, useHub } from '../hub';
import { changelogPath } from '../model';
import { topicsFor } from '../topics';
@@ -63,7 +63,7 @@ export default function ProjectChangelog({ loaderData }: Route.ComponentProps) {
}
facts={
<>
-
+
{releases[0] && }
{first && releases.length > 1 && }
{owner && (
diff --git a/site/app/routes/projects.tsx b/site/app/routes/projects.tsx
index 1c52449..180e2c2 100644
--- a/site/app/routes/projects.tsx
+++ b/site/app/routes/projects.tsx
@@ -7,7 +7,7 @@ import { Fact, loadRail, PageHeader, StandardRail } from '../components/kit/page
import Status from '../components/Status';
import { Page } from '../components/Shell';
import { hubContext } from '../context';
-import { day } from '../format';
+import { day, plural } from '../format';
import { useHub } from '../hub';
import { STATUS_LABEL } from '../model';
import { useSpotlight, useTilt } from '../motion/motion';
@@ -89,7 +89,7 @@ function Entry({
<>
v{latest.version} {day(latest.date)}
- {releases.length} {releases.length === 1 ? 'release' : 'releases'}
+ {releases.length} {plural(releases.length, 'release')}
>
) : (
@@ -147,7 +147,7 @@ export default function Projects({ loaderData }: Route.ComponentProps) {
a.name.localeCompare(b.name),
);
const slugs = new Set(all.map((project) => project.slug));
- const latest = hub.releases.find((release) => slugs.has(release.project));
+ const released = hub.releases.filter((release) => slugs.has(release.project));
const pickTag = (item: string) => setTag((current) => (current === item ? null : item));
return (
@@ -168,12 +168,9 @@ export default function Projects({ loaderData }: Route.ComponentProps) {
}
facts={
<>
-
- slugs.has(r.project)).length}
- label="releases"
- />
- {latest && }
+
+
+ {released[0] && }
{tag && (