Skip to content

Commit 0313e38

Browse files
feat(web): printable project table card with a QR code
/projects/:slug/table-card shows the logo, title, summary, a QR code to the project page, "Scan to join this project", the URL as text and the Slack channel. Print and Back controls stay off the page; in print the card scales up to fill one Letter page. The code encodes the origin the card was opened on, so production prints codeforphilly.org. Fixes #232 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TGqwPjt3v4eMfiHpPmCKnR
1 parent dbe9e41 commit 0313e38

3 files changed

Lines changed: 84 additions & 0 deletions

File tree

‎apps/web/src/App.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ProjectsIndex } from '@/screens/ProjectsIndex';
1010
import { ProjectDetail } from '@/screens/ProjectDetail';
1111
import { ProjectEdit } from '@/screens/ProjectEdit';
1212
import { ProjectBuzzNew } from '@/screens/ProjectBuzzNew';
13+
import { ProjectTableCard } from '@/screens/ProjectTableCard';
1314
import { PeopleIndex } from '@/screens/PeopleIndex';
1415
import { PersonDetail } from '@/screens/PersonDetail';
1516
import { ProfileEdit } from '@/screens/ProfileEdit';
@@ -50,6 +51,7 @@ const router = createBrowserRouter([
5051
{ path: '/projects/create', element: <ProjectEdit mode="create" /> },
5152
{ path: '/projects/:slug', element: <ProjectDetail /> },
5253
{ path: '/projects/:slug/edit', element: <ProjectEdit mode="edit" /> },
54+
{ path: '/projects/:slug/table-card', element: <ProjectTableCard /> },
5355
{ path: '/projects/:slug/updates/:number', element: <ProjectDetail anchor="update" /> },
5456
{ path: '/projects/:slug/buzz/:buzzSlug', element: <ProjectDetail anchor="buzz" /> },
5557
{ path: '/projects/:slug/buzz/new', element: <ProjectBuzzNew /> },

‎apps/web/src/screens/ProjectDetail.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,9 @@ export function ProjectDetail({ anchor }: ProjectDetailProps = {}) {
603603
>
604604
Share to Slack
605605
</Button>
606+
<Button asChild variant="outline">
607+
<Link to={`/projects/${slug}/table-card`}>Table card</Link>
608+
</Button>
606609
</div>
607610
</section>
608611

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { Link, useParams } from 'react-router';
3+
import { QRCodeSVG } from 'qrcode.react';
4+
import { Button } from '@/components/ui/button';
5+
import { api, ApiError } from '@/lib/api';
6+
7+
/**
8+
* Printable table sign for a project (specs/screens/project-table-card.md):
9+
* a QR code to the project page, where a newcomer can sign in and join.
10+
*/
11+
export function ProjectTableCard() {
12+
const { slug = '' } = useParams<{ slug: string }>();
13+
const projectQ = useQuery({
14+
queryKey: ['project', slug],
15+
queryFn: () => api.projects.get(slug),
16+
});
17+
18+
if (projectQ.isLoading) {
19+
return <div className="container mx-auto px-4 py-12 text-muted-foreground">Loading project…</div>;
20+
}
21+
22+
if (projectQ.isError) {
23+
const err = projectQ.error;
24+
if (err instanceof ApiError && err.status === 404) {
25+
return (
26+
<div className="container mx-auto px-4 py-16 text-center">
27+
<h1 className="text-2xl font-bold mb-2">Project not found</h1>
28+
<p className="text-muted-foreground mb-6">No project with the slug “{slug}” exists.</p>
29+
<Link to="/projects" className="text-primary hover:underline">
30+
← Browse all projects
31+
</Link>
32+
</div>
33+
);
34+
}
35+
return <div className="container mx-auto px-4 py-12 text-destructive">Failed to load project.</div>;
36+
}
37+
38+
const project = projectQ.data!.data;
39+
// The origin the card was opened on, so production prints codeforphilly.org
40+
// and a test instance prints its own host.
41+
const url = `${window.location.origin}/projects/${project.slug}`;
42+
const displayUrl = url.replace(/^https?:\/\//, '');
43+
44+
return (
45+
<div className="container mx-auto px-4 py-8 print:p-0">
46+
<div className="flex items-center justify-center gap-3 mb-6 print:hidden">
47+
<Button onClick={() => window.print()}>Print</Button>
48+
<Button asChild variant="outline">
49+
<Link to={`/projects/${project.slug}`}>Back to project</Link>
50+
</Button>
51+
</div>
52+
53+
<article className="mx-auto max-w-2xl rounded-lg border border-border bg-white text-black px-8 py-12 text-center print:max-w-none print:border-0 print:rounded-none print:py-0">
54+
<img
55+
src="/img/logo-horizontal.svg"
56+
alt="Code for Philly"
57+
className="mx-auto h-14 mb-10 print:h-20"
58+
/>
59+
<h1 className="text-4xl font-bold leading-tight mb-3 print:text-5xl">{project.title}</h1>
60+
{project.summary && <p className="text-lg text-neutral-700 mb-8 line-clamp-2">{project.summary}</p>}
61+
<QRCodeSVG
62+
value={url}
63+
size={288}
64+
marginSize={0}
65+
title={`QR code for ${displayUrl}`}
66+
className="mx-auto my-8 h-[3in] w-[3in] print:my-12 print:h-[4in] print:w-[4in]"
67+
/>
68+
<p className="text-3xl font-semibold mb-2 print:text-4xl">Scan to join this project</p>
69+
<p className="text-lg font-mono text-neutral-700 break-all">{displayUrl}</p>
70+
{project.links.chatChannel && (
71+
<p className="text-lg mt-6">
72+
Chat: <span className="font-semibold">#{project.links.chatChannel}</span> on the Code
73+
for Philly Slack
74+
</p>
75+
)}
76+
</article>
77+
</div>
78+
);
79+
}

0 commit comments

Comments
 (0)