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
75 changes: 44 additions & 31 deletions src/app/bills/BillExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useEffect, useMemo, useState, useCallback, memo } from "react";
import { BillSummary } from "./types";
import BillCard from "@/app/bills/components/BillCard";
import SectionLabel from "@/components/SectionLabel";
import {
FilterSidebar,
FilterState,
Expand Down Expand Up @@ -264,39 +265,51 @@ function BillExplorer({ bills }: BillExplorerProps) {
}, []);

return (
<div className="mx-auto max-w-7xl py-4 md:py-6">
<div className="flex flex-col gap-4 md:flex-row md:gap-6">
<aside className="w-full md:w-auto md:min-w-[260px] md:max-w-xs md:shrink-0">
<FilterSidebar
filters={filters}
onFiltersChange={setFilters}
onClearFilters={clearFilters}
forceCollapsed={isFilterCollapsed}
onCollapsedChange={setIsFilterCollapsed}
filterOptions={filterOptions}
/>
</aside>

<main className="flex-1">
{filteredBills.length === 0 ? (
<div className="text-sm">No bills match your filters.</div>
) : (
<ul className="flex flex-col gap-3">
{filteredBills.map((bill) => (
<BillCard
key={bill.billID}
bill={
bill as BillSummary & {
tenet_evaluations?: TenetEvaluation[];
<section className="px-5 py-10">
<div className="mx-auto max-w-[1080px]">
<div className="flex flex-col gap-6 md:flex-row md:gap-8">
<aside className="w-full md:w-[280px] md:shrink-0">
<FilterSidebar
filters={filters}
onFiltersChange={setFilters}
onClearFilters={clearFilters}
forceCollapsed={isFilterCollapsed}
onCollapsedChange={setIsFilterCollapsed}
filterOptions={filterOptions}
/>
</aside>

<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-4 border-b border-border-light pb-3">
<SectionLabel as="h2">Bills</SectionLabel>
<span className="type-label-sm text-text-secondary">
{filteredBills.length}{" "}
{filteredBills.length === 1 ? "bill" : "bills"}
</span>
</div>

{filteredBills.length === 0 ? (
<p className="type-body-sm py-6 text-text-secondary">
No bills match your filters.
</p>
) : (
<ul className="mt-6 flex flex-col gap-3">
{filteredBills.map((bill) => (
<BillCard
key={bill.billID}
bill={
bill as BillSummary & {
tenet_evaluations?: TenetEvaluation[];
}
}
}
/>
))}
</ul>
)}
</main>
/>
))}
</ul>
)}
</div>
</div>
</div>
</div>
</section>
);
}

Expand Down
44 changes: 22 additions & 22 deletions src/app/bills/FAQModalTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function FAQModalTrigger() {
<button
ref={buttonRef}
type="button"
className="mt-3 inline-block cursor-pointer text-xs underline text-text-secondary hover:text-dark"
className="type-label cursor-pointer text-text-secondary underline underline-offset-4 hover:text-accent"
onClick={openModal}
>
FAQ
Expand All @@ -73,7 +73,7 @@ export default function FAQModalTrigger() {
aria-modal="true"
aria-labelledby="faq-title"
tabIndex={-1}
className={`relative z-10 w-full max-w-2xl border border-border-light bg-white p-5 shadow-xl outline-none transition-all duration-200 ease-out ${
className={`relative z-10 max-h-[85vh] w-full max-w-2xl overflow-y-auto border border-border-light bg-bg p-6 shadow-xl outline-none transition-all duration-200 ease-out ${
visible
? "opacity-100 translate-y-0 scale-100"
: "opacity-0 translate-y-2 scale-95"
Expand All @@ -86,63 +86,63 @@ export default function FAQModalTrigger() {
<button
type="button"
aria-label="Close"
className="ml-3 text-text-secondary hover:text-dark"
className="ml-3 cursor-pointer text-text-secondary hover:text-accent"
onClick={closeModal}
>
✕
</button>
</div>

<div className="mt-4 text-sm leading-6">
<h3 className="font-semibold">Why did you build this?</h3>
<p className="text-text-secondary mt-1">
<div className="mt-5">
<h3 className="type-h4 text-dark">Why did you build this?</h3>
<p className="type-default mt-1.5 text-text-secondary">
We built this tool so that Canadians could easily understand
parliamentary bills and how they align with a pro-growth stance.
</p>

<h3 className="font-semibold mt-4">
<h3 className="type-h4 mt-6 text-dark">
Where does the bill data come from?
</h3>
<p className="text-text-secondary mt-1">
<p className="type-default mt-1.5 text-text-secondary">
We are powered by{" "}
<a
href="https://civicsproject.org"
target="_blank"
rel="noreferrer"
className="underline"
className="text-accent underline"
>
The Civics Project
</a>
, which pulls data from the Government of Canada&apos;s open
parliamentary data feeds.
</p>

<h3 className="font-semibold mt-4">
<h3 className="type-h4 mt-6 text-dark">
How do you determine a bill&apos;s judgement?
</h3>
<p className="text-text-secondary mt-1">
<p className="type-default mt-1.5 text-text-secondary">
Each bill is evaluated against a set of pro-growth principles
using an LLM. The LLM uses these principles to make a judgement
on the bill. We show all of this, including the principles,
evaluation and rationale, on the bill&apos;s page.
</p>

<h3 className="font-semibold mt-4">
<h3 className="type-h4 mt-6 text-dark">
Was this tool created to vote on bills?
</h3>
<p className="text-text-secondary mt-1">
<p className="type-default mt-1.5 text-text-secondary">
No. This tool was created to help Canadians better understand
bills and their economic impact. It is meant to be educational
and informative.
</p>

<h3 className="font-semibold mt-4">
<h3 className="type-h4 mt-6 text-dark">
What prompt do you use to evaluate the bills?
</h3>
<p className="text-text-secondary mt-1">
<p className="type-default mt-1.5 text-text-secondary">
This project, including the prompt, is open sourced on{" "}
<a
className="underline"
className="text-accent underline"
href="https://github.com/BuildCanada/BillsTracker/blob/main/src/prompt/summary-and-vote-prompt.ts"
target="_blank"
rel="noreferrer"
Expand All @@ -152,12 +152,12 @@ export default function FAQModalTrigger() {
.
</p>

<h3 className="font-semibold mt-4">How can I contribute?</h3>
<p className="text-text-secondary mt-1">
<h3 className="type-h4 mt-6 text-dark">How can I contribute?</h3>
<p className="type-default mt-1.5 text-text-secondary">
This is a work in progress and we would love help from others.
Join us on{" "}
<a
className="underline"
className="text-accent underline"
href="https://discord.gg/VmbBSXKMve"
target="_blank"
rel="noreferrer"
Expand All @@ -167,10 +167,10 @@ export default function FAQModalTrigger() {
.
</p>

<h3 className="font-semibold mt-4">How can I get in touch?</h3>
<p className="text-text-secondary mt-1">
<h3 className="type-h4 mt-6 text-dark">How can I get in touch?</h3>
<p className="type-default mt-1.5 text-text-secondary">
You can reach out to us at{" "}
<a className="underline" href="mailto:hi@buildcanada.com">
<a className="text-accent underline" href="mailto:hi@buildcanada.com">
hi@buildcanada.com
</a>
</p>
Expand Down
144 changes: 69 additions & 75 deletions src/app/bills/components/BillCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,94 +13,88 @@ interface BillCardProps {
bill: BillSummary & { tenet_evaluations?: TenetEvaluation[] };
}

/* A hairline tag in the house style: square, mono, uppercase. */
function Tag({
children,
emphasis = false,
}: {
children: React.ReactNode;
emphasis?: boolean;
}) {
return (
<span
className={`inline-flex items-center gap-1.5 border px-2 py-1 type-label-sm ${
emphasis
? "border-accent/40 bg-auburn-50 text-accent"
: "border-border-light text-text-secondary"
}`}
>
{children}
</span>
);
}

function BillCard({ bill }: BillCardProps) {
const bestDate = getBillMostRecentDate(bill);
const dateDisplay = formatBillDate(bestDate);

const judgementValue: JudgementValue = bill.final_judgment || "abstain";
const isProForma = bill.billID === "C-1" || bill.billID === "S-1";

return (
<li className="group border border-border-light bg-white duration-200 overflow-hidden hover:border-dark transition-colors">
<Link href={`${BASE_PATH}/${bill.billID}`} className="block">
<div className="p-5">
{/* Header Section */}
<div className="flex items-start md:flex-row flex-col-reverse justify-between gap-4 mb-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2"></div>
<h2 className="text-xl mb-4 font-semibold max-w-[70%] transition-colors leading-tight">
{bill.shortTitle ?? bill.title}
</h2>
</div>

{bill.final_judgment && <Judgement judgement={judgementValue} />}
<li className="border border-border-light bg-white transition-colors hover:border-dark">
<Link
href={`${BASE_PATH}/${bill.billID}`}
className="group block h-full p-5 transition-colors hover:bg-linen-50"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<p className="type-label text-text-secondary">
Bill {bill.billID}
{bill.sponsorName && bill.sponsorName !== "Unknown" && (
<span className="text-text-muted"> &middot; {bill.sponsorName}</span>
)}
</p>
<h2 className="type-h4 mt-2 text-dark transition-colors group-hover:text-accent">
{bill.shortTitle ?? bill.title}
</h2>
</div>

{/* Description */}
{bill.description && (
<p
className="text-sm text-text-secondary mb-3 leading-relaxed overflow-hidden"
style={{
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical" as const,
}}
>
{bill.description}
</p>
{bill.final_judgment && (
<Judgement judgement={judgementValue} className="shrink-0" />
)}
</div>

{/* Tags Section */}
<div className="flex flex-wrap gap-1.5 mb-4">
{/* Impact Badge */}
{bill.impact && (
<span
className={`inline-flex items-center px-2.5 py-1 type-label-sm ${
bill.impact === "High"
? "bg-auburn-100 text-auburn-800"
: bill.impact === "Medium"
? "bg-linen-200 text-charcoal-700"
: "bg-charcoal-100 text-charcoal-600"
}`}
>
{bill.impact} Impact
</span>
)}
{(bill.billID === "C-1" || bill.billID === "S-1") && (
<span className="inline-flex items-center px-2.5 py-1 type-label-sm bg-charcoal-100 text-charcoal-600">
Pro Forma Bill
</span>
)}
{bill.description && (
<p className="type-default mt-2 line-clamp-2 text-text-secondary">
{bill.description}
</p>
)}

{/* Genre Tags (limit to 3 visible) */}
{bill.genres &&
bill.genres.length > 0 &&
bill.genres.map((genre, index) => {
const icon = getCategoryIcon(genre);
return (
icon && (
<span
key={index}
className="inline-flex items-center gap-1 px-2 py-1 type-label-sm bg-linen-100 text-charcoal-700"
>
<DynamicIcon
className="w-4 h-4 mr-1"
name={icon as ComponentProps<typeof DynamicIcon>["name"]}
/>{" "}
{genre}
</span>
)
);
})}
</div>
<div className="mt-4 flex flex-wrap items-center gap-1.5">
{bill.impact && <Tag emphasis={bill.impact === "High"}>{bill.impact} impact</Tag>}
{isProForma && <Tag>Pro forma</Tag>}
{bill.genres?.map((genre, index) => {
const icon = getCategoryIcon(genre);
return (
icon && (
<Tag key={index}>
<DynamicIcon
className="h-3.5 w-3.5"
name={icon as ComponentProps<typeof DynamicIcon>["name"]}
/>
{genre}
</Tag>
)
);
})}
</div>

{/* Footer Section */}
<div className="flex items-center justify-between text-xs text-text-secondary pt-2 border-t border-border-light">
<div className="flex items-center gap-4">
<span className="text-xs text-dark">Bill {bill.billID}</span>{" "}
{bill.sponsorName && <span>by {bill.sponsorName}</span>}
</div>
<span className="text-text-secondary">{dateDisplay}</span>
</div>
<div className="mt-4 flex items-center justify-between gap-3 border-t border-border-light pt-3">
<span className="type-label-sm text-text-secondary transition-colors group-hover:text-accent">
Read the analysis
</span>
<span className="type-mono-sm text-text-secondary">{dateDisplay}</span>
</div>
</Link>
</li>
Expand Down
Loading
Loading