Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ GOOGLE_CLIENT_SECRET=your-google-client-secret
# Required for database connection
MONGO_URI=mongodb://localhost:27017/billstracker

# Production DB connection used with local development
PROD_MONGO_URI=

# Civics Project API
# Required for fetching bill data
CIVICS_PROJECT_API_KEY=your-civics-project-api-key
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ next-env.d.ts
# Claude Code
.claude
.plans
.mcp.json
.mcp.json

# DB Backups
db-backups
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"format": "biome check --write",
"test": "vitest run",
"type-check": "tsc --noEmit",
"check": "npm run type-check && npm run format && npx biome check --error-on-warnings",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
Expand Down Expand Up @@ -81,6 +82,7 @@
"@types/react-dom": "19.1.9",
"@vitest/ui": "^3.2.4",
"daisyui": "^5.1.7",
"dotenv": "^17.2.3",
"happy-dom": "^18.0.1",
"husky": "^9.1.7",
"tailwindcss": "^4",
Expand Down
2,637 changes: 1,311 additions & 1,326 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

17 changes: 1 addition & 16 deletions src/app/BillExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
FilterOptions,
} from "@/components/FilterSection/filter-section.component";
import { useIsMobile } from "@/components/ui/use-mobile";
import { shouldShowDetermination } from "@/utils/should-show-determination/should-show-determination.util";
import type { JudgementValue } from "@/components/Judgement/judgement.component";
import { TenetEvaluation } from "@/models/Bill";

Expand Down Expand Up @@ -107,21 +106,7 @@ function BillExplorer({ bills }: BillExplorerProps) {
// Filter bills
const filteredBills = useMemo(() => {
const filtered = bills.filter((bill) => {
const judgementParams: Parameters<typeof shouldShowDetermination>[0] = {
vote: bill.final_judgment,
isSocialIssue: bill.isSocialIssue,
tenetEvaluations: bill.tenet_evaluations,
};

const shouldDisplayDetermination =
shouldShowDetermination(judgementParams);
const normalizedFinalJudgement: JudgementValue =
judgementParams.vote === "yes" || judgementParams.vote === "no"
? judgementParams.vote
: "neutral";
const displayJudgement: JudgementValue = shouldDisplayDetermination
? normalizedFinalJudgement
: "neutral";
const displayJudgement: JudgementValue = bill.final_judgment || "abstain";

// Search
if (filters.search.trim()) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default async function EditBillPage({ params }: Params) {
</div>
<div className="space-y-2">
<label className="block text-sm font-medium" htmlFor="final_judgment">
Final Judgment (yes/no/neutral)
Final Judgment (yes/no/abstain)
</label>
<textarea
id="final_judgment"
Expand Down
7 changes: 4 additions & 3 deletions src/app/[id]/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ export default async function OpengraphImage({
params: { id: string };
}) {
const bill = await getUnifiedBillById(params.id);
const status = (bill?.final_judgment || "abstain").toLowerCase();
const voteText =
bill?.final_judgment === "yes"
status === "yes"
? "Vote: Yes"
: bill?.final_judgment === "no"
: status === "no"
? "Vote: No"
: "Vote: Neutral";
: "Vote: Abstain";
const textForFont = `${bill?.short_title || bill?.title || params.id} ${voteText} ${PROJECT_NAME} Build Canada Policy Tracker Powered by The Civics Project`;
let interRegular: ArrayBuffer | undefined;
let interBold: ArrayBuffer | undefined;
Expand Down
22 changes: 7 additions & 15 deletions src/app/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,10 @@ export default async function BillDetail({ params }: Params) {
);
}

const isSocialIssue = unifiedBill.isSocialIssue;
const judgementParams = {
vote: unifiedBill.final_judgment,
isSocialIssue,
tenetEvaluations: unifiedBill.tenet_evaluations,
} as const;
const shouldDisplayDetermination = shouldShowDetermination(judgementParams);
const normalizedFinalJudgement: JudgementValue =
judgementParams.vote === "yes" || judgementParams.vote === "no"
? judgementParams.vote
: "neutral";
const showAnalysis = shouldDisplayDetermination;
const shouldDisplayDetermination = shouldShowDetermination(
unifiedBill.final_judgment,
);
const judgementValue: JudgementValue = unifiedBill.final_judgment;

return (
<div className="mx-auto max-w-[1100px] px-6 py-8">
Expand Down Expand Up @@ -118,13 +110,13 @@ export default async function BillDetail({ params }: Params) {
<BillSummary bill={unifiedBill} />
<BillAnalysis
bill={unifiedBill}
showAnalysis={showAnalysis}
showAnalysis={shouldDisplayDetermination}
displayJudgement={{
value: normalizedFinalJudgement,
value: judgementValue,
shouldDisplay: shouldDisplayDetermination,
}}
/>
{showAnalysis &&
{shouldDisplayDetermination &&
unifiedBill.question_period_questions &&
unifiedBill.question_period_questions.length > 0 && (
<BillQuestions
Expand Down
2 changes: 1 addition & 1 deletion src/app/bills/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default async function EditBillPage({ params }: Params) {
</div>
<div className="space-y-2">
<label className="block text-sm font-medium" htmlFor="final_judgment">
Final Judgment (yes/no/neutral)
Final Judgment (yes/no/abstain)
</label>
<textarea
id="final_judgment"
Expand Down
2 changes: 0 additions & 2 deletions src/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ImageResponse } from "next/og";
import { PROJECT_NAME } from "@/consts/general";

export const runtime = "nodejs";

Expand Down Expand Up @@ -101,4 +100,3 @@ export default async function HomeOpengraphImage() {
},
);
}

51 changes: 25 additions & 26 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { BUILD_CANADA_TWITTER_HANDLE, PROJECT_NAME } from "@/consts/general";
import FAQModalTrigger from "./FAQModalTrigger";

const CANADIAN_PARLIAMENT_NUMBER = 45;
type HomeSearchParams = { cache?: string };

// Force runtime generation (avoid build-time pre-render) and cache in-memory for 5 minutes
export const dynamic = "force-dynamic";
Expand All @@ -23,10 +24,12 @@ export async function generateMetadata(): Promise<Metadata> {
"Understand Canadian federal bills with builder-first analysis.";
const h = headers();
const headerList = await h;
const host = headerList.get("x-forwarded-host") || headerList.get("host") || "";
const host =
headerList.get("x-forwarded-host") || headerList.get("host") || "";
const proto = (headerList.get("x-forwarded-proto") || "https").split(",")[0];
const baseUrl =
env.NEXT_PUBLIC_APP_URL || (host ? `${proto}://${host}` : "http://localhost:3000");
env.NEXT_PUBLIC_APP_URL ||
(host ? `${proto}://${host}` : "http://localhost:3000");
const pagePath = buildRelativePath();
const pageUrl = `${baseUrl}${pagePath}`;
const ogPath = buildRelativePath("opengraph-image");
Expand All @@ -42,7 +45,9 @@ export async function generateMetadata(): Promise<Metadata> {
url: pageUrl,
siteName: PROJECT_NAME,
type: "website",
images: [{ url: ogImageUrl, width: 1200, height: 630, alt: PROJECT_NAME }],
images: [
{ url: ogImageUrl, width: 1200, height: 630, alt: PROJECT_NAME },
],
},
twitter: {
card: "summary_large_image",
Expand All @@ -65,6 +70,7 @@ export async function generateMetadata(): Promise<Metadata> {
};
}

const shouldUseLocalCache = process.env.NODE_ENV === "production";
let mergedBillsCache: { data: BillSummary[]; expiresAt: number } | null = null;

async function getApiBills(): Promise<BillSummary[]> {
Expand Down Expand Up @@ -116,24 +122,14 @@ async function getMergedBills(): Promise<BillSummary[]> {
const dbBill = dbBillsMap.get(apiBill.billID);

if (dbBill) {
const alignCount = (dbBill.tenet_evaluations ?? []).filter(
(t) => t.alignment === "aligns",
).length;
const conflictCount = (dbBill.tenet_evaluations ?? []).filter(
(t) => t.alignment === "conflicts",
).length;
const displayFinal: BillSummary["final_judgment"] =
alignCount === 1 && conflictCount === 0
? "neutral"
: (dbBill.final_judgment as BillSummary["final_judgment"]);
// Merge API bill with DB data (DB data takes precedence for analysis fields)
return {
...dbBill,
...apiBill,
shortTitle: dbBill.short_title || apiBill.shortTitle,
summary: dbBill.summary,
isSocialIssue: dbBill.isSocialIssue,
final_judgment: displayFinal,
final_judgment: dbBill.final_judgment as BillSummary["final_judgment"],
rationale: dbBill.rationale,
needs_more_info: dbBill.needs_more_info,
missing_details: dbBill.missing_details,
Expand All @@ -151,16 +147,6 @@ async function getMergedBills(): Promise<BillSummary[]> {
for (const [billId, dbBill] of dbBillsMap) {
if (!mergedBills.find((bill) => bill.billID === billId)) {
// Convert DB bill to BillSummary format
const alignCount = (dbBill.tenet_evaluations ?? []).filter(
(t) => t.alignment === "aligns",
).length;
const conflictCount = (dbBill.tenet_evaluations ?? []).filter(
(t) => t.alignment === "conflicts",
).length;
const displayFinal: BillSummary["final_judgment"] =
alignCount === 1 && conflictCount === 0
? "neutral"
: (dbBill.final_judgment as BillSummary["final_judgment"]);
const billSummary: BillSummary = {
billID: dbBill.billId,
title: dbBill.title,
Expand All @@ -179,7 +165,7 @@ async function getMergedBills(): Promise<BillSummary[]> {
dbBill.lastUpdatedOn?.toISOString() || new Date().toISOString(),
summary: dbBill.summary,
isSocialIssue: dbBill.isSocialIssue,
final_judgment: displayFinal,
final_judgment: dbBill.final_judgment as BillSummary["final_judgment"],
rationale: dbBill.rationale,
needs_more_info: dbBill.needs_more_info,
missing_details: dbBill.missing_details,
Expand All @@ -198,6 +184,12 @@ async function getMergedBills(): Promise<BillSummary[]> {
}

async function getMergedBillsCached(): Promise<BillSummary[]> {
if (!shouldUseLocalCache) {
// Avoid stale data while iterating locally; always hit the backing store.
mergedBillsCache = null;
return getMergedBills();
}

const now = Date.now();
const ttlMs = 300 * 1000; // 5 minutes
if (mergedBillsCache && mergedBillsCache.expiresAt > now) {
Expand All @@ -208,7 +200,14 @@ async function getMergedBillsCached(): Promise<BillSummary[]> {
return data;
}

export default async function Home() {
export default async function Home({
searchParams,
}: {
searchParams?: HomeSearchParams;
}) {
if (searchParams?.cache === "clear") {
mergedBillsCache = null; // Allow manual cache busting with ?cache=clear
}
const bills = await getMergedBillsCached();
return (
<div className="min-h-screen">
Expand Down
2 changes: 1 addition & 1 deletion src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface BillSummary {
alignment?: "Economy" | "Health" | "Environment" | "Security" | "Other";
// Additional fields from MongoDB/analysis
summary?: string;
final_judgment?: "yes" | "no" | "neutral";
final_judgment?: "yes" | "no" | "abstain";
rationale?: string;
needs_more_info?: boolean;
missing_details?: string[];
Expand Down
22 changes: 2 additions & 20 deletions src/components/BillCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import dayjs from "dayjs";
import { getCategoryIcon } from "@/utils/bill-category-to-icon/bill-category-to-icon.util";
import { getBillStageDates } from "@/utils/stages-to-dates/stages-to-dates";
import { TenetEvaluation } from "@/models/Bill";
import { shouldShowDetermination } from "@/utils/should-show-determination/should-show-determination.util";

interface BillCardProps {
bill: BillSummary & { tenet_evaluations?: TenetEvaluation[] };
Expand All @@ -20,19 +19,7 @@ function BillCard({ bill }: BillCardProps) {
? dayjs(lastUpdated).format("MMM D, YYYY")
: "N/A";

const judgementParams = {
vote: bill.final_judgment,
isSocialIssue: bill.isSocialIssue,
tenetEvaluations: bill.tenet_evaluations,
} as const;
const shouldDisplayDetermination = shouldShowDetermination(judgementParams);
const normalizedFinalJudgement: JudgementValue =
judgementParams.vote === "yes" || judgementParams.vote === "no"
? judgementParams.vote
: "neutral";
const judgementValue: JudgementValue = shouldDisplayDetermination
? normalizedFinalJudgement
: "neutral";
const judgementValue: JudgementValue = bill.final_judgment || "abstain";

return (
<li className="group rounded-lg border bg-[var(--panel)] shadow-sm duration-200 overflow-hidden">
Expand All @@ -47,12 +34,7 @@ function BillCard({ bill }: BillCardProps) {
</h2>
</div>

{bill.final_judgment && (
<Judgement
judgement={judgementValue}
isSocialIssue={bill.isSocialIssue}
/>
)}
{bill.final_judgment && <Judgement judgement={judgementValue} />}
</div>

{/* Description */}
Expand Down
12 changes: 3 additions & 9 deletions src/components/BillDetail/BillAnalysis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function BillAnalysis({
}: BillAnalysisProps) {
const judgementValue: JudgementValue = displayJudgement.shouldDisplay
? displayJudgement.value
: "neutral";
: "abstain";

if (!showAnalysis) {
return (
Expand All @@ -61,10 +61,7 @@ export function BillAnalysis({
</CardHeader>
<CardContent>
<div>
<Judgement
isSocialIssue={bill.isSocialIssue}
judgement={judgementValue}
/>
<Judgement judgement={judgementValue} />
</div>
</CardContent>
</Card>
Expand All @@ -79,10 +76,7 @@ export function BillAnalysis({
<div className="flex md:items-center md:justify-between md:flex-row flex-col gap-4 ">
<CardTitle className="mb-2">Builder Assessment</CardTitle>
<div>
<Judgement
isSocialIssue={bill.isSocialIssue}
judgement={judgementValue}
/>
<Judgement judgement={judgementValue} />
</div>
</div>
</CardHeader>
Expand Down
2 changes: 1 addition & 1 deletion src/components/FilterSection/filter-section.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export function FilterSidebar({
<div className="space-y-3 ">
<Label>Judgement</Label>
<div className="space-y-2">
{["yes", "no", "neutral"].map((j) => (
{["yes", "no", "abstain"].map((j) => (
<div key={j} className="flex items-start space-x-2">
<Checkbox
id={`judgement-${j}`}
Expand Down
Loading