Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions public/trade-barriers/buildcanada-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/trade-barriers/og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 41 additions & 25 deletions src/app/memos/MemoResultsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,49 @@ import { useMemosFilter } from "./store";
import { MemoItem, formatDate, shortenName } from "./types";

function MemoGridRow({ memo, basePath }: { memo: MemoItem; basePath: string }) {
const author = memo.author;
const hasMedia = Boolean(author?.photo);
return (
<Link
href={`${basePath}/${memo.slug}`}
className="flex flex-col border-b border-r border-border-light group hover:bg-linen-50 transition-colors"
>
<div className="grid grid-cols-[7rem_1fr] wide:grid-cols-[6.125rem_1fr] flex-1">
<div className="relative overflow-hidden aspect-square bg-border-light">
{memo.author.photo && (
<Image
src={memo.author.photo}
alt={memo.author.name}
width={64}
height={64}
className="absolute inset-0 w-full h-full object-cover"
unoptimized
/>
)}
</div>
<div
className={
hasMedia
? "grid grid-cols-[7rem_1fr] wide:grid-cols-[6.125rem_1fr] flex-1"
: "flex-1"
}
>
{hasMedia && (
<div className="relative overflow-hidden aspect-square bg-border-light">
{author?.photo && (
<Image
src={author.photo}
alt={author.name}
width={64}
height={64}
className="absolute inset-0 w-full h-full object-cover"
unoptimized
/>
)}
</div>
)}

<div className="min-w-0 p-5">
<h3 className="type-h4 group-hover:text-accent transition-colors line-clamp-1">
{memo.title}
</h3>
<div className="flex items-center gap-2 mt-1">
<p className="type-label text-text-secondary">
<span className="hidden wide:inline">{memo.author.name}</span>
<span className="wide:hidden">
{shortenName(memo.author.name)}
</span>
</p>
<span className="text-text-secondary">&middot;</span>
{author && (
<>
<p className="type-label text-text-secondary">
<span className="hidden wide:inline">{author.name}</span>
<span className="wide:hidden">{shortenName(author.name)}</span>
</p>
<span className="text-text-secondary">&middot;</span>
</>
)}
<p className="type-label-sm text-text-secondary">
{formatDate(memo.publishedAt, memo.createdAt)}
</p>
Expand Down Expand Up @@ -69,9 +81,11 @@ function MemoGridRow({ memo, basePath }: { memo: MemoItem; basePath: string }) {
export default function MemoResultsList({
memos,
basePath = "/memos",
resultsLabel = "Memos",
}: {
memos: MemoItem[];
basePath?: string;
resultsLabel?: string;
}) {
const search = useMemosFilter((s) => s.search);
const activeCategory = useMemosFilter((s) => s.activeCategory);
Expand All @@ -87,28 +101,30 @@ export default function MemoResultsList({
list = list.filter(
(m) =>
m.title.toLowerCase().includes(q) ||
m.author.name.toLowerCase().includes(q) ||
m.author?.name.toLowerCase().includes(q) ||
m.keyMessage1?.toLowerCase().includes(q)
);
}

return list;
}, [memos, search, activeCategory]);

const lowerLabel = resultsLabel.toLowerCase();

return (
<div className="animate-fade-in" style={{ animationDelay: "1.6s" }}>
<section className="px-5 py-10 border-b border-border-light">
<div className="max-w-[1080px] mx-auto">
<SectionLabel as="h2" className={activeCategory ? "capitalize" : undefined}>
{activeCategory
? `${activeCategory.replace(/-/g, " ")} Memos`
: "All Memos"}
? `${activeCategory.replace(/-/g, " ")} ${resultsLabel}`
: `All ${resultsLabel}`}
</SectionLabel>
{filtered.length === 0 && (
<p className="type-body-sm text-text-secondary py-4">
{memos.length === 0
? "No memos yet."
: "No memos match your filters."}
? `No ${lowerLabel} yet.`
: `No ${lowerLabel} match your filters.`}
</p>
)}
<div className="grid grid-cols-1 wide:grid-cols-2 wide:gap-x-0 border-t border-l border-border-light mt-4">
Expand Down
4 changes: 2 additions & 2 deletions src/app/memos/MemoSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import SectionLabel from "@/components/SectionLabel";
import { useMemosFilter } from "./store";

export default function MemoSearch() {
export default function MemoSearch({ placeholder = "Search memos..." }: { placeholder?: string } = {}) {
const search = useMemosFilter((s) => s.search);
const setSearch = useMemosFilter((s) => s.setSearch);

Expand Down Expand Up @@ -38,7 +38,7 @@ export default function MemoSearch() {
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search memos..."
placeholder={placeholder}
className="flex-1 bg-transparent font-mono text-base tracking-normal outline-none placeholder:text-text-secondary"
/>
{search && (
Expand Down
18 changes: 12 additions & 6 deletions src/app/memos/MemosListClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
if (category && categories.includes(category)) {
setActiveCategory(category);
}
}, []);

Check warning on line 23 in src/app/memos/MemosListClient.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has missing dependencies: 'categories', 'searchParams', and 'setActiveCategory'. Either include them or remove the dependency array

return null;
}
Expand All @@ -28,9 +28,13 @@
export default function MemosListClient({
memos,
basePath = "/memos",
showCategoryFilter = true,
resultsLabel,
}: {
memos: MemoItem[];
basePath?: string;
showCategoryFilter?: boolean;
resultsLabel?: string;
}) {
const categories = useMemo(() => {
const cats = new Set<string>();
Expand All @@ -42,13 +46,15 @@

return (
<>
<Suspense fallback={null}>
<CategoryFromSearchParams categories={categories} />
</Suspense>
{showCategoryFilter && (
<Suspense fallback={null}>
<CategoryFromSearchParams categories={categories} />
</Suspense>
)}
<FeaturedHero memos={memos} basePath={basePath} />
<CategoryFilter categories={categories} />
<MemoSearch />
<MemoResultsList memos={memos} basePath={basePath} />
{showCategoryFilter && <CategoryFilter categories={categories} />}
<MemoSearch placeholder={`Search ${(resultsLabel ?? "memos").toLowerCase()}...`} />
<MemoResultsList memos={memos} basePath={basePath} resultsLabel={resultsLabel} />
</>
);
}
2 changes: 1 addition & 1 deletion src/app/memos/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface MemoItem {
author: {
name: string;
photo: string | null;
};
} | null;
keyMessage1: string | null;
keyMessage2: string | null;
keyMessage3: string | null;
Expand Down
149 changes: 149 additions & 0 deletions src/app/posts/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import Link from "next/link";
import { fetchPost, fetchPosts, getSiteConfig } from "@/lib/api";
import { extractHeadings } from "@/lib/extract-headings";
import { ShareSection } from "@/components/share";
import { Signpost } from "@/components/custom/signpost";
import { SubscribeButton } from "@/components/ui/subscribe-button";
import { buildGraph } from "@/lib/schemas/graph";
import { generateBreadcrumbSchema } from "@/lib/schemas/generators/breadcrumb";
import { generateOrganizationSchema } from "@/lib/schemas/generators/organization";

export async function generateStaticParams() {
try {
const posts = await fetchPosts();
return posts.map((p) => ({ slug: p.slug }));
} catch {
return [];
}
}

export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
let post;
try {
post = await fetchPost(slug);
} catch {
return { title: "Post Not Found | Build Canada" };
}

const title = `${post.title} | Build Canada`;
const description = post.summary ?? undefined;
const image = post.seoImage || undefined;

return {
title,
description,
openGraph: {
title,
description,
type: "article",
publishedTime: post.publishedAt ?? post.createdAt,
...(image && { images: [{ url: image }] }),
},
twitter: {
card: "summary_large_image",
title,
description,
...(image && { images: [image] }),
},
};
}

export default async function PostDetailPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
let post;
try {
post = await fetchPost(slug);
} catch {
notFound();
}

const date = new Date(post.publishedAt || post.createdAt).toLocaleDateString(
"en-CA",
{ year: "numeric", month: "long", day: "numeric" },
);

const configData = getSiteConfig();
const fullUrl = `${configData.siteUrl}/posts/${post.slug}`;

const jsonLd = buildGraph(
generateOrganizationSchema(configData),
generateBreadcrumbSchema(`/posts/${post.slug}`, post.title, configData.siteUrl),
);

const { headings, html: bodyHtml } = extractHeadings(post.body ?? "");

const sidebar = (
<div className="space-y-5">
<ShareSection title={post.title} url={fullUrl} />
<SubscribeButton variant="primary" source="inline" className="w-full">
Subscribe
</SubscribeButton>
</div>
);

return (
<div className="mx-[10px] my-[10px] border border-border-light bg-bg">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>

<div className="max-w-[1400px] mx-auto w-full px-[5vw] py-10 md:px-[10vw]">
<Link
href="/posts"
className="type-label text-text-secondary hover:text-dark transition-colors flex items-center gap-1.5 mb-6 py-1"
>
<svg width="12" height="12" viewBox="0 0 14 14" fill="none">
<path
d="M12 7H3M6 3L2 7l4 4"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
All posts
</Link>

<h1 className="type-title mb-4 max-w-[720px]">{post.title}</h1>
<p className="type-label text-text-secondary">{date}</p>
</div>

<div
className="animate-fade-in max-w-[1400px] mx-auto px-[5vw] md:px-[10vw] pt-4 pb-[52px] 2xl-memo:grid 2xl-memo:grid-cols-[240px_minmax(0,1fr)] 2xl-memo:gap-12"
style={{ animationDelay: "0.3s" }}
>
<Signpost headings={headings} shareTitle={post.title} shareUrl={fullUrl} />

<article className="max-w-[720px]" data-memo-content>
{post.summary && (
<div className="mb-8 p-6 border-[3px] border-double border-border-light bg-[#f0e5dc]">
<span className="type-label block mb-3">Summary</span>
<p className="type-body whitespace-pre-line">{post.summary}</p>
</div>
)}

<div
className="prose-bc"
dangerouslySetInnerHTML={{ __html: bodyHtml }}
/>

<div className="print-hide 2xl-memo:hidden mt-10 pt-8 border-t border-border-light">
{sidebar}
</div>
</article>
</div>
</div>
);
}
Loading
Loading