Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9c4f228
Add First Nations financial data pages
xrendan Feb 1, 2026
ac22bdb
Add amountScalingFactor prop to Sankey components
xrendan Feb 1, 2026
835f55a
Fix build error for First Nations pages with null values
xrendan Feb 2, 2026
d9cc400
Add source document viewer to First Nations pages
xrendan Feb 2, 2026
fee842a
Add land claims table to First Nations pages
xrendan Feb 2, 2026
394031f
Fix Sankey totals for First Nations pages
xrendan Feb 3, 2026
1d1a1bf
Add external link to Land Claims data source and show claim status
xrendan Feb 3, 2026
d24a904
Redesign First Nations index page with filterable table
xrendan Feb 3, 2026
48d8178
Fix React key warning in ClaimsTable
xrendan Feb 3, 2026
ab6076e
Fix hydration mismatch by adding font class to [lang] layout
xrendan Feb 3, 2026
46361a8
Remove unused Plus_Jakarta_Sans font
xrendan Feb 3, 2026
7598516
Remove max height constraint from First Nations table
xrendan Feb 3, 2026
65f5bc7
Fix TypeError when hovering over difference block in Sankey chart
xrendan Feb 3, 2026
b81c75a
Improve First Nations table with filters, legend, and linking
xrendan Feb 3, 2026
f8547e0
Add population data display and sorting to First Nations table
xrendan Feb 3, 2026
dd9ab6a
Complete all 106 missing French translations
xrendan Feb 3, 2026
19b5c64
Add retry logic for 5xx errors in supabaseFetch
xrendan Feb 3, 2026
0866faf
Rename "band/bands" to "First Nations" throughout codebase
xrendan Feb 3, 2026
edce207
Add collapsible FAQ section to First Nations index page
xrendan Feb 4, 2026
74a19c4
Add population history support and update translations
xrendan Feb 4, 2026
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
2 changes: 1 addition & 1 deletion data/static-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"data/provincial/alberta/2025/summary.json": "2026-01-31T18:15:04.397Z",
"data/provincial/alberta/2024/summary.json": "2026-01-31T18:15:04.395Z",
"data/provincial/british-columbia/2025/summary.json": "2026-01-31T18:15:04.697Z",
"data/provincial/ontario/2025/summary.json": "2026-01-31T20:09:30.573Z",
"data/provincial/ontario/2025/summary.json": "2026-01-31T23:47:54.378Z",
"data/provincial/ontario/2024/summary.json": "2026-01-31T18:15:04.703Z",
"data/municipal/alberta/edmonton/2024/summary.json": "2026-01-31T18:15:04.153Z",
"data/municipal/british-columbia/vancouver/2024/summary.json": "2026-01-31T18:15:04.299Z",
Expand Down
2 changes: 2 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: true,
},
// Transpile @buildcanada/charts which exports TypeScript source
transpilePackages: ["@buildcanada/charts"],
// Enable MDX Support For .mdx Files
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
experimental: {
Expand Down
83 changes: 83 additions & 0 deletions src/app/[lang]/(main)/first-nations/[bcid]/[year]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { notFound } from "next/navigation";
import { initLingui } from "@/initLingui";
import { FirstNationsPageContent } from "@/components/first-nations";
import {
getAllFirstNations,
getClaimsByFirstNation,
getFirstNationById,
getFirstNationYearData,
} from "@/lib/supabase";
import { locales } from "@/lib/constants";

export const revalidate = 3600;
export const dynamicParams = true;

export async function generateStaticParams() {
const firstNations = await getAllFirstNations();

// Pre-generate pages for all First Nations with all their available years
return locales.flatMap((lang) =>
firstNations.flatMap((firstNation) =>
firstNation.availableYears.map((year) => ({
lang,
bcid: firstNation.bcid,
year,
})),
),
);
}

export default async function FirstNationYearPage({
params,
}: {
params: Promise<{ lang: string; bcid: string; year: string }>;
}) {
const { lang, bcid, year } = await params;
initLingui(lang);

const firstNation = await getFirstNationById(bcid);

if (!firstNation) {
notFound();
}

// Check if the requested year is valid for this First Nation
if (!firstNation.availableYears.includes(year)) {
notFound();
}

const [firstNationYearData, claims] = await Promise.all([
getFirstNationYearData(bcid, year),
getClaimsByFirstNation(bcid),
]);

const {
statementOfOperations,
statementOfFinancialPosition,
remuneration,
notes,
} = firstNationYearData;

// If no data at all, show not found
if (
!statementOfOperations &&
!statementOfFinancialPosition &&
!remuneration &&
!notes
) {
notFound();
}

return (
<FirstNationsPageContent
firstNation={firstNation}
year={year}
statementOfOperations={statementOfOperations}
statementOfFinancialPosition={statementOfFinancialPosition}
remuneration={remuneration}
notes={notes}
claims={claims}
lang={lang}
/>
);
}
90 changes: 90 additions & 0 deletions src/app/[lang]/(main)/first-nations/[bcid]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { redirect, notFound } from "next/navigation";
import { Trans } from "@lingui/react/macro";
import { initLingui } from "@/initLingui";
import { getFirstNationById, getAllFirstNations } from "@/lib/supabase";
import { locales } from "@/lib/constants";
import {
H1,
Intro,
Page,
PageContent,
Section,
InternalLink,
} from "@/components/Layout";

export const revalidate = 3600;
export const dynamicParams = true;

export async function generateStaticParams() {
const firstNations = await getAllFirstNations();

return locales.flatMap((lang) =>
firstNations.map((firstNation) => ({
lang,
bcid: firstNation.bcid,
})),
);
}

export default async function FirstNationOverviewPage({
params,
}: {
params: Promise<{ lang: string; bcid: string }>;
}) {
const { lang, bcid } = await params;

const firstNation = await getFirstNationById(bcid);

if (!firstNation) {
notFound();
}

const latestYear = firstNation.availableYears[0];

// If First Nation has data, redirect to latest year
if (latestYear) {
redirect(`/${lang}/first-nations/${bcid}/${latestYear}`);
}

// Otherwise show a "no data" page
initLingui(lang);

return (
<Page>
<PageContent>
<Section>
<H1>{firstNation.name}</H1>
<Intro>
<Trans>
{firstNation.name} is a First Nation
{firstNation.province ? ` in ${firstNation.province}` : ""}.
</Trans>
</Intro>
</Section>
<Section>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-8 text-center">
<p className="text-gray-600 mb-4">
<Trans>
No financial data is currently available for {firstNation.name}.
</Trans>
</p>
<p className="text-sm text-gray-500 mb-6">
<Trans>
Financial data from annual reports published under the First
Nations Financial Transparency Act will appear here once
available.
</Trans>
</p>
<InternalLink
href="/first-nations"
lang={lang}
className="inline-block px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition-colors"
>
<Trans>Browse all First Nations</Trans>
</InternalLink>
</div>
</Section>
</PageContent>
</Page>
);
}
53 changes: 53 additions & 0 deletions src/app/[lang]/(main)/first-nations/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Trans } from "@lingui/react/macro";
import { initLingui } from "@/initLingui";
import { H1, Intro, Page, PageContent, Section } from "@/components/Layout";
import {
FirstNationsFAQ,
FirstNationsSearch,
} from "@/components/first-nations";
import { getAllFirstNations } from "@/lib/supabase";
import { locales } from "@/lib/constants";

export const revalidate = 3600; // Revalidate every hour
export const dynamicParams = true;

export async function generateStaticParams() {
return locales.map((lang) => ({ lang }));
}

export default async function FirstNationsIndexPage({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
initLingui(lang);

const firstNations = await getAllFirstNations();

return (
<Page>
<PageContent>
<Section>
<H1>
<Trans>First Nations Financial Data</Trans>
</H1>
<Intro>
<Trans>
Explore financial data from First Nations across Canada. Data is
extracted from annual reports published under the First Nations
Financial Transparency Act (FNFTA), including statements of
operations, financial position, and remuneration.
</Trans>
</Intro>
</Section>
<Section>
<FirstNationsFAQ />
</Section>
<Section>
<FirstNationsSearch firstNations={firstNations} lang={lang} />
</Section>
</PageContent>
</Page>
);
}
11 changes: 1 addition & 10 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
import { allMessages } from "@/appRouterI18n";
import { LinguiClientProvider } from "@/components/LinguiClientProvider";
import { initLingui } from "@/initLingui";
import { cn } from "@/lib/utils";
import { Analytics } from "@vercel/analytics/next";
import { Plus_Jakarta_Sans } from "next/font/google";
import { ReactNode } from "react";
import { Toaster } from "sonner";
import "./[lang]/globals.css";
import { PostHogProvider } from "./[lang]/providers";

// Root layout must provide <html> and <body> tags
// Default to 'en' for root layout (actual lang is handled by [lang]/layout)
const plusJakartaSans = Plus_Jakarta_Sans({
weight: ["600", "700"],
subsets: ["latin"],
});

export default function RootLayout({ children }: { children: ReactNode }) {
// Initialize with default language for root layout
// The [lang] layout will override this with the actual language
initLingui("en");

return (
<html lang="en">
<body className={cn("antialiased", plusJakartaSans.className)}>
<body className="antialiased">
<PostHogProvider>
<LinguiClientProvider
initialLocale="en"
Expand Down
6 changes: 5 additions & 1 deletion src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import { cn, localizedPath } from "@/lib/utils";
export const Section = ({
children,
className = "",
id,
}: {
children: React.ReactNode;
className?: string;
id?: string;
}) => {
return (
<div className={cn(`mt-8 max-w-5xl mx-auto`, className)}>{children}</div>
<div id={id} className={cn(`mt-8 max-w-5xl mx-auto`, className)}>
{children}
</div>
);
};

Expand Down
10 changes: 10 additions & 0 deletions src/components/MainLayout/_components/DesktopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export default function DesktopNav(props: DesktopNavProps) {
const spendingActive =
pathname.startsWith(`/${i18n.locale}/spending`) ||
pathname.startsWith(`/${i18n.locale}/budget`) ||
pathname.startsWith(`/${i18n.locale}/first-nations`) ||
(firstSegment ? jurisdictionSlugsSet.has(firstSegment) : false);

return (
Expand Down Expand Up @@ -169,6 +170,15 @@ export default function DesktopNav(props: DesktopNavProps) {
</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>

<DropdownMenu.Item asChild>
<Link
href={`/${i18n.locale}/first-nations`}
className="px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground rounded cursor-pointer"
>
<Trans>First Nations</Trans>
</Link>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
Expand Down
12 changes: 12 additions & 0 deletions src/components/MainLayout/_components/MobileMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ export function MobileMenu(props: MobileMenuProps) {
))}
</div>
))}

{/* First Nations */}
<MobileNavLink
href={`/${i18n.locale}/first-nations`}
active={pathname.startsWith(`/${i18n.locale}/first-nations`)}
onClick={() => setIsMenuOpen(false)}
>
<span className="pl-4 inline-block">
<Trans>First Nations</Trans>
</span>
</MobileNavLink>

<MobileNavLink
href={`/${i18n.locale}/tax-visualizer`}
active={pathname === `/${i18n.locale}/tax-visualizer`}
Expand Down
10 changes: 7 additions & 3 deletions src/components/Sankey/JurisdictionSankey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { SankeyData } from "./SankeyChartD3";

export function JurisdictionSankey({
data,
// jurisdictionSlug,
amountScalingFactor,
}: {
data: SankeyData;
// jurisdictionSlug?: string;
// Scaling factor for amounts. Use 1e9 (default) for data in billions,
// 1e6 for millions, or 1 for raw values (no scaling).
amountScalingFactor?: number;
// Kept for backwards compatibility - not currently used
jurisdictionSlug?: string;
}) {
return <SankeyChart data={data} />;
return <SankeyChart data={data} amountScalingFactor={amountScalingFactor} />;
}
6 changes: 5 additions & 1 deletion src/components/Sankey/SankeyChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const getFlatData = (data: SankeyData) => {
};

const chartHeight = 760;
const amountScalingFactor = 1e9;
const DEFAULT_AMOUNT_SCALING_FACTOR = 1e9;

const chartConfig = {
revenue: {
Expand Down Expand Up @@ -100,9 +100,13 @@ const chartConfig = {
export type SankeyChartProps = {
data: SankeyData;
showDepartmentLinks?: boolean;
// Scaling factor for amounts. Use 1e9 (default) for data in billions,
// 1e6 for millions, or 1 for raw values (no scaling).
amountScalingFactor?: number;
};

export function SankeyChart(props: SankeyChartProps) {
const { amountScalingFactor = DEFAULT_AMOUNT_SCALING_FACTOR } = props;
const { i18n } = useLingui();
const [chartData, setChartData] = useState<SankeyData | null>(null);
const [flatData, setFlatData] = useState<FlatDataNodes | null>(null);
Expand Down
2 changes: 2 additions & 0 deletions src/components/Sankey/SankeyChartD3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,8 @@ export class SankeyChartD3 {
source: null,
target: null,
link: null,
pathToRoot: [],
descendants: [],
});
}

Expand Down
Loading