From 1517d78cc279c5021801d471c52ec0556a57a617 Mon Sep 17 00:00:00 2001 From: dukesvrtech Date: Fri, 31 Jan 2025 07:53:46 -0500 Subject: [PATCH 01/18] Mobile: Navigation changes --- app/components/Header.tsx | 94 +++++++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 24 deletions(-) diff --git a/app/components/Header.tsx b/app/components/Header.tsx index cc679a0..ef3b50d 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -1,9 +1,56 @@ +'use client' + import Link from 'next/link' -import { Scale } from 'lucide-react' +import { Scale, Menu } from 'lucide-react' import { ThemeToggle } from './ThemeToggle' import { SearchDialog } from './search/SearchDialog' +import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet' +import { useState } from 'react' + +const NavLinks = ({ onClick }: { onClick?: () => void }) => ( + +) export default function Header() { + const [open, setOpen] = useState(false) + return (
@@ -11,34 +58,33 @@ export default function Header() { LegiEquity -
) From e6b343608800de31890a17ba3570b67bb8cfba79 Mon Sep 17 00:00:00 2001 From: dukesvrtech Date: Fri, 31 Jan 2025 08:11:06 -0500 Subject: [PATCH 02/18] Navigation done --- app/components/Header.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/components/Header.tsx b/app/components/Header.tsx index ef3b50d..9f73bd5 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -4,7 +4,7 @@ import Link from 'next/link' import { Scale, Menu } from 'lucide-react' import { ThemeToggle } from './ThemeToggle' import { SearchDialog } from './search/SearchDialog' -import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet' +import { Sheet, SheetContent, SheetTrigger, SheetHeader } from '@/components/ui/sheet' import { useState } from 'react' const NavLinks = ({ onClick }: { onClick?: () => void }) => ( @@ -71,7 +71,6 @@ export default function Header() { {/* Mobile Navigation */}
- - +
+ +
+
+ +
From 9671fbc47b5bd76530c74d7dddda2387d32cc051 Mon Sep 17 00:00:00 2001 From: dukesvrtech Date: Fri, 31 Jan 2025 08:16:10 -0500 Subject: [PATCH 03/18] Fix linting issuees --- app/components/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 9f73bd5..3cc7cb0 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -4,7 +4,7 @@ import Link from 'next/link' import { Scale, Menu } from 'lucide-react' import { ThemeToggle } from './ThemeToggle' import { SearchDialog } from './search/SearchDialog' -import { Sheet, SheetContent, SheetTrigger, SheetHeader } from '@/components/ui/sheet' +import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet' import { useState } from 'react' const NavLinks = ({ onClick }: { onClick?: () => void }) => ( From bb7760f722443a326d98b628f95efeaf43761dfe Mon Sep 17 00:00:00 2001 From: dukesvrtech Date: Fri, 31 Jan 2025 09:58:36 -0500 Subject: [PATCH 04/18] Integrate Open-Graph --- app/[state]/page.tsx | 55 +++++++++ app/api/og/route.tsx | 234 ++++++++++++++++++++++++++++++++++++++ app/page.tsx | 28 +++++ package-lock.json | 205 +++++++++++++++++++++++++++++++++ package.json | 1 + public/images/og-home.png | Bin 0 -> 38754 bytes 6 files changed, 523 insertions(+) create mode 100644 app/api/og/route.tsx create mode 100644 public/images/og-home.png diff --git a/app/[state]/page.tsx b/app/[state]/page.tsx index 4f5b2a4..df081cb 100644 --- a/app/[state]/page.tsx +++ b/app/[state]/page.tsx @@ -1,3 +1,4 @@ +import { Metadata } from 'next' import db from "@/lib/db"; import { BillList } from "@/app/components/BillList"; import Pagination from "@/app/components/Pagination"; @@ -8,6 +9,60 @@ import { BillFiltersWrapper } from "@/app/components/filters/BillFiltersWrapper" import type { BillFilters as BillFiltersType, PartyType } from "@/app/types/filters"; import { CheckCircle, AlertCircle, MinusCircle } from "lucide-react"; +// State name mapping for metadata +const STATE_NAMES: { [key: string]: string } = { + 'AL': 'Alabama', 'AK': 'Alaska', 'AZ': 'Arizona', 'AR': 'Arkansas', + 'CA': 'California', 'CO': 'Colorado', 'CT': 'Connecticut', 'DE': 'Delaware', + 'FL': 'Florida', 'GA': 'Georgia', 'HI': 'Hawaii', 'ID': 'Idaho', + 'IL': 'Illinois', 'IN': 'Indiana', 'IA': 'Iowa', 'KS': 'Kansas', + 'KY': 'Kentucky', 'LA': 'Louisiana', 'ME': 'Maine', 'MD': 'Maryland', + 'MA': 'Massachusetts', 'MI': 'Michigan', 'MN': 'Minnesota', 'MS': 'Mississippi', + 'MO': 'Missouri', 'MT': 'Montana', 'NE': 'Nebraska', 'NV': 'Nevada', + 'NH': 'New Hampshire', 'NJ': 'New Jersey', 'NM': 'New Mexico', 'NY': 'New York', + 'NC': 'North Carolina', 'ND': 'North Dakota', 'OH': 'Ohio', 'OK': 'Oklahoma', + 'OR': 'Oregon', 'PA': 'Pennsylvania', 'RI': 'Rhode Island', 'SC': 'South Carolina', + 'SD': 'South Dakota', 'TN': 'Tennessee', 'TX': 'Texas', 'UT': 'Utah', + 'VT': 'Vermont', 'VA': 'Virginia', 'WA': 'Washington', 'WV': 'West Virginia', + 'WI': 'Wisconsin', 'WY': 'Wyoming', 'DC': 'District of Columbia' +}; + +type Props = { + params: { state: string } + searchParams: { [key: string]: string | string[] | undefined } +} + +export async function generateMetadata({ params }: Props): Promise { + const stateCode = params.state.toUpperCase() + const stateName = STATE_NAMES[stateCode] || stateCode + + return { + title: `${stateName} Bills - LegiEquity`, + description: `Analyze the demographic impact of ${stateName} legislation. View bills and their effects on age, disability, gender, race, and religious groups.`, + openGraph: { + title: `${stateName} Legislative Analysis - LegiEquity`, + description: `Analyze the demographic impact of ${stateName} legislation. View bills and their effects on age, disability, gender, race, and religious groups.`, + url: `https://legiequity.us/${params.state.toLowerCase()}`, + siteName: 'LegiEquity', + images: [ + { + url: `https://legiequity.us/api/og?state=${stateCode}`, // You'll need to create this API route + width: 1200, + height: 630, + alt: `${stateName} Legislative Analysis`, + }, + ], + locale: 'en_US', + type: 'website', + }, + twitter: { + card: 'summary_large_image', + title: `${stateName} Legislative Analysis - LegiEquity`, + description: `Analyze the demographic impact of ${stateName} legislation. View bills and their effects on age, disability, gender, race, and religious groups.`, + images: [`https://legiequity.us/api/og?state=${stateCode}`], + }, + } +} + async function getBills( stateCode: string, page = 1, diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx new file mode 100644 index 0000000..f030027 --- /dev/null +++ b/app/api/og/route.tsx @@ -0,0 +1,234 @@ +import { ImageResponse } from '@vercel/og' +import { NextRequest } from 'next/server' + +export const runtime = 'edge' + +// Complete state name mapping +const STATE_NAMES: { [key: string]: string } = { + // Federal + 'US': 'United States Congress', + + // States + 'AL': 'Alabama', + 'AK': 'Alaska', + 'AZ': 'Arizona', + 'AR': 'Arkansas', + 'CA': 'California', + 'CO': 'Colorado', + 'CT': 'Connecticut', + 'DE': 'Delaware', + 'FL': 'Florida', + 'GA': 'Georgia', + 'HI': 'Hawaii', + 'ID': 'Idaho', + 'IL': 'Illinois', + 'IN': 'Indiana', + 'IA': 'Iowa', + 'KS': 'Kansas', + 'KY': 'Kentucky', + 'LA': 'Louisiana', + 'ME': 'Maine', + 'MD': 'Maryland', + 'MA': 'Massachusetts', + 'MI': 'Michigan', + 'MN': 'Minnesota', + 'MS': 'Mississippi', + 'MO': 'Missouri', + 'MT': 'Montana', + 'NE': 'Nebraska', + 'NV': 'Nevada', + 'NH': 'New Hampshire', + 'NJ': 'New Jersey', + 'NM': 'New Mexico', + 'NY': 'New York', + 'NC': 'North Carolina', + 'ND': 'North Dakota', + 'OH': 'Ohio', + 'OK': 'Oklahoma', + 'OR': 'Oregon', + 'PA': 'Pennsylvania', + 'RI': 'Rhode Island', + 'SC': 'South Carolina', + 'SD': 'South Dakota', + 'TN': 'Tennessee', + 'TX': 'Texas', + 'UT': 'Utah', + 'VT': 'Vermont', + 'VA': 'Virginia', + 'WA': 'Washington', + 'WV': 'West Virginia', + 'WI': 'Wisconsin', + 'WY': 'Wyoming', + + // District of Columbia + 'DC': 'District of Columbia' +} + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url) + const stateCode = searchParams.get('state')?.toUpperCase() + + // Handle missing state parameter + if (!stateCode) { + return new ImageResponse( + ( +
+
+ + + + + LegiEquity + +
+
+ AI-powered analysis of legislation's impact on demographic equity +
+
+ ), + { + width: 1200, + height: 630, + }, + ) + } + + // Get state name + const stateName = STATE_NAMES[stateCode] || stateCode + + // Special handling for US Congress + if (stateCode === 'US') { + return new ImageResponse( + ( +
+
+ {/* US Congress Seal */} + US Congress +
+ + + + + LegiEquity + +
+
+
+ Congressional Legislative Analysis +
+
+ Analyzing demographic impact of federal legislation across age, disability, gender, race, and religion +
+
+ ), + { + width: 1200, + height: 630, + }, + ) + } + + // State-specific image with SVG + return new ImageResponse( + ( +
+
+ {/* State SVG */} + {stateName} +
+ + + + + LegiEquity + +
+
+
+ {stateName} Legislative Analysis +
+
+ Analyzing demographic impact of legislation across age, disability, gender, race, and religion +
+
+ ), + { + width: 1200, + height: 630, + }, + ) + } catch (e: any) { + console.log(`${e.message}`) + return new Response(`Failed to generate the image`, { + status: 500, + }) + } +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 7502243..4d8bcff 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,3 +1,4 @@ +import { Metadata } from 'next' import { AuroraBackground } from "@/app/components/ui/aurora-background"; import StateTiles from "@/app/components/StateTiles"; import AnimatedContent from "./components/AnimatedContent"; @@ -9,6 +10,33 @@ const InteractiveMap = dynamic(() => import("./components/InteractiveMap"), { ssr: false, }); +export const metadata: Metadata = { + title: 'LegiEquity - AI-Powered Legislative Impact Analysis', + description: 'Understand how bills and laws affect communities across age, disability, gender, race, and religion through AI-powered analysis.', + openGraph: { + title: 'LegiEquity - AI-Powered Legislative Impact Analysis', + description: 'Understand how bills and laws affect communities across age, disability, gender, race, and religion through AI-powered analysis.', + url: 'https://legiequity.us', + siteName: 'LegiEquity', + images: [ + { + url: 'https://legiequity.us/images/og-home.png', // You'll need to create this image + width: 1200, + height: 630, + alt: 'LegiEquity - Legislative Impact Analysis', + }, + ], + locale: 'en_US', + type: 'website', + }, + twitter: { + card: 'summary_large_image', + title: 'LegiEquity - AI-Powered Legislative Impact Analysis', + description: 'Understand how bills and laws affect communities across age, disability, gender, race, and religion through AI-powered analysis.', + images: ['https://legiequity.us/images/og-home.png'], // Same image as OG + }, +} + export default function Home() { return (
diff --git a/package-lock.json b/package-lock.json index 7a09993..25ceb94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.7", "@types/lodash": "^4.17.15", + "@vercel/og": "^0.6.5", "chart.js": "^4.4.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", @@ -1686,6 +1687,15 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, + "node_modules/@resvg/resvg-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz", + "integrity": "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==", + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1700,6 +1710,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -2048,6 +2074,20 @@ "dev": true, "license": "ISC" }, + "node_modules/@vercel/og": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@vercel/og/-/og-0.6.5.tgz", + "integrity": "sha512-GFXtgid3+TcVHTd668a10vGpzAh4Ty/yBZPRxKf1UicI8Vi8EthfvSxcaLW0KvQBBe1+d7TcjecLZHRT8JzQ4g==", + "license": "MPL-2.0", + "dependencies": { + "@resvg/resvg-wasm": "2.4.0", + "satori": "0.12.1", + "yoga-wasm-web": "0.3.3" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -2415,6 +2455,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2530,6 +2579,15 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001692", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", @@ -2769,6 +2827,47 @@ "node": ">= 8" } }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-gradient-parser": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz", + "integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3202,6 +3301,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3731,6 +3836,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4243,6 +4354,18 @@ "node": ">= 0.4" } }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -4938,6 +5061,16 @@ "url": "https://github.com/sponsors/antonk52" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -5615,6 +5748,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5628,6 +5767,16 @@ "node": ">=6" } }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6312,6 +6461,34 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/satori": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.12.1.tgz", + "integrity": "sha512-0SbjchvDrDbeXeQgxWVtSWxww7qcFgk3DtSE2/blHOSlLsSHwIqO2fCrtVa/EudJ7Eqno8A33QNx56rUyGbLuw==", + "license": "MPL-2.0", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-gradient-parser": "^0.0.16", + "css-to-react-native": "^3.0.0", + "emoji-regex": "^10.2.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-wasm-web": "^0.3.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/satori/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -6634,6 +6811,12 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6976,6 +7159,12 @@ "node": ">=0.8" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7226,6 +7415,16 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -7587,6 +7786,12 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoga-wasm-web": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", + "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", + "license": "MIT" } } } diff --git a/package.json b/package.json index 423f131..bf870da 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.7", "@types/lodash": "^4.17.15", + "@vercel/og": "^0.6.5", "chart.js": "^4.4.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", diff --git a/public/images/og-home.png b/public/images/og-home.png new file mode 100644 index 0000000000000000000000000000000000000000..2a64bd28eb4974f76ffbe90e310e1eed1cc008f1 GIT binary patch literal 38754 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 literal 0 HcmV?d00001 From 22ac544a7f1e50a4cb9aad617925d34e88a81402 Mon Sep 17 00:00:00 2001 From: dukesvrtech Date: Fri, 31 Jan 2025 10:09:52 -0500 Subject: [PATCH 05/18] OG on Bill Detail Pages --- app/[state]/bill/[id]/page.tsx | 51 ++++++++++++++++++++++++++++ app/[state]/page.tsx | 18 +--------- app/api/og/route.tsx | 62 +--------------------------------- 3 files changed, 53 insertions(+), 78 deletions(-) diff --git a/app/[state]/bill/[id]/page.tsx b/app/[state]/bill/[id]/page.tsx index 14e1225..9c6c4e0 100644 --- a/app/[state]/bill/[id]/page.tsx +++ b/app/[state]/bill/[id]/page.tsx @@ -7,6 +7,8 @@ import SponsorList from './SponsorList'; import RollCallVotes from './RollCallVotes'; import BillAnalysis from './BillAnalysis'; import { Footer } from "@/app/components/layout/Footer"; +import { Metadata } from 'next' +import { STATE_NAMES } from '@/app/constants/states'; export const revalidate = 3600; // Revalidate every hour @@ -114,6 +116,55 @@ interface BillAnalysis { }>; } +interface Props { + params: { state: string; id: string } +} + +export async function generateMetadata({ params }: Props): Promise { + const stateCode = params.state.toUpperCase(); + const bill = await getBill(stateCode, params.id); + + if (!bill) { + return { + title: 'Bill Not Found - LegiEquity', + description: 'The requested bill could not be found.', + } + } + + const stateName = STATE_NAMES[stateCode] || stateCode; + const title = `${bill.bill_number}${bill.title !== bill.description ? `: ${bill.title}` : ''}`; + const description = bill.description.length > 200 + ? bill.description.substring(0, 197) + '...' + : bill.description; + + return { + title: `${title} - ${stateName} Legislature - LegiEquity`, + description, + openGraph: { + title: `${title} - ${stateName} Legislature`, + description, + url: `https://legiequity.us/${params.state.toLowerCase()}/bill/${params.id}`, + siteName: 'LegiEquity', + images: [ + { + url: `https://legiequity.us/api/og/bill?state=${stateCode}&id=${params.id}`, + width: 1200, + height: 630, + alt: `${bill.bill_number} - ${stateName} Legislature`, + }, + ], + locale: 'en_US', + type: 'article', + }, + twitter: { + card: 'summary_large_image', + title: `${title} - ${stateName} Legislature`, + description, + images: [`https://legiequity.us/api/og/bill?state=${stateCode}&id=${params.id}`], + }, + } +} + async function getBillDocuments(billId: string): Promise { const [texts, supplements] = await Promise.all([ // Get bill texts diff --git a/app/[state]/page.tsx b/app/[state]/page.tsx index df081cb..d4220d3 100644 --- a/app/[state]/page.tsx +++ b/app/[state]/page.tsx @@ -8,23 +8,7 @@ import { Footer } from "@/app/components/layout/Footer"; import { BillFiltersWrapper } from "@/app/components/filters/BillFiltersWrapper"; import type { BillFilters as BillFiltersType, PartyType } from "@/app/types/filters"; import { CheckCircle, AlertCircle, MinusCircle } from "lucide-react"; - -// State name mapping for metadata -const STATE_NAMES: { [key: string]: string } = { - 'AL': 'Alabama', 'AK': 'Alaska', 'AZ': 'Arizona', 'AR': 'Arkansas', - 'CA': 'California', 'CO': 'Colorado', 'CT': 'Connecticut', 'DE': 'Delaware', - 'FL': 'Florida', 'GA': 'Georgia', 'HI': 'Hawaii', 'ID': 'Idaho', - 'IL': 'Illinois', 'IN': 'Indiana', 'IA': 'Iowa', 'KS': 'Kansas', - 'KY': 'Kentucky', 'LA': 'Louisiana', 'ME': 'Maine', 'MD': 'Maryland', - 'MA': 'Massachusetts', 'MI': 'Michigan', 'MN': 'Minnesota', 'MS': 'Mississippi', - 'MO': 'Missouri', 'MT': 'Montana', 'NE': 'Nebraska', 'NV': 'Nevada', - 'NH': 'New Hampshire', 'NJ': 'New Jersey', 'NM': 'New Mexico', 'NY': 'New York', - 'NC': 'North Carolina', 'ND': 'North Dakota', 'OH': 'Ohio', 'OK': 'Oklahoma', - 'OR': 'Oregon', 'PA': 'Pennsylvania', 'RI': 'Rhode Island', 'SC': 'South Carolina', - 'SD': 'South Dakota', 'TN': 'Tennessee', 'TX': 'Texas', 'UT': 'Utah', - 'VT': 'Vermont', 'VA': 'Virginia', 'WA': 'Washington', 'WV': 'West Virginia', - 'WI': 'Wisconsin', 'WY': 'Wyoming', 'DC': 'District of Columbia' -}; +import { STATE_NAMES } from '@/app/constants/states'; type Props = { params: { state: string } diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx index f030027..93c7759 100644 --- a/app/api/og/route.tsx +++ b/app/api/og/route.tsx @@ -1,69 +1,9 @@ import { ImageResponse } from '@vercel/og' import { NextRequest } from 'next/server' +import { STATE_NAMES } from '@/app/constants/states'; export const runtime = 'edge' -// Complete state name mapping -const STATE_NAMES: { [key: string]: string } = { - // Federal - 'US': 'United States Congress', - - // States - 'AL': 'Alabama', - 'AK': 'Alaska', - 'AZ': 'Arizona', - 'AR': 'Arkansas', - 'CA': 'California', - 'CO': 'Colorado', - 'CT': 'Connecticut', - 'DE': 'Delaware', - 'FL': 'Florida', - 'GA': 'Georgia', - 'HI': 'Hawaii', - 'ID': 'Idaho', - 'IL': 'Illinois', - 'IN': 'Indiana', - 'IA': 'Iowa', - 'KS': 'Kansas', - 'KY': 'Kentucky', - 'LA': 'Louisiana', - 'ME': 'Maine', - 'MD': 'Maryland', - 'MA': 'Massachusetts', - 'MI': 'Michigan', - 'MN': 'Minnesota', - 'MS': 'Mississippi', - 'MO': 'Missouri', - 'MT': 'Montana', - 'NE': 'Nebraska', - 'NV': 'Nevada', - 'NH': 'New Hampshire', - 'NJ': 'New Jersey', - 'NM': 'New Mexico', - 'NY': 'New York', - 'NC': 'North Carolina', - 'ND': 'North Dakota', - 'OH': 'Ohio', - 'OK': 'Oklahoma', - 'OR': 'Oregon', - 'PA': 'Pennsylvania', - 'RI': 'Rhode Island', - 'SC': 'South Carolina', - 'SD': 'South Dakota', - 'TN': 'Tennessee', - 'TX': 'Texas', - 'UT': 'Utah', - 'VT': 'Vermont', - 'VA': 'Virginia', - 'WA': 'Washington', - 'WV': 'West Virginia', - 'WI': 'Wisconsin', - 'WY': 'Wyoming', - - // District of Columbia - 'DC': 'District of Columbia' -} - export async function GET(req: NextRequest) { try { const { searchParams } = new URL(req.url) From 766151298f9f22696153d53e46c7ed67cc3c3bee Mon Sep 17 00:00:00 2001 From: dukesvrtech Date: Fri, 31 Jan 2025 10:46:09 -0500 Subject: [PATCH 06/18] Added OG to bill details page --- app/api/og/bill/route.tsx | 117 ++++++++++++++++++++++++++++++++++++++ app/api/og/route.tsx | 4 +- app/constants/states.ts | 59 +++++++++++++++++++ components/ui/dialog.tsx | 1 - 4 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 app/api/og/bill/route.tsx create mode 100644 app/constants/states.ts delete mode 100644 components/ui/dialog.tsx diff --git a/app/api/og/bill/route.tsx b/app/api/og/bill/route.tsx new file mode 100644 index 0000000..3a0c3b6 --- /dev/null +++ b/app/api/og/bill/route.tsx @@ -0,0 +1,117 @@ +import { ImageResponse } from '@vercel/og' +import { NextRequest } from 'next/server' +import { STATE_NAMES } from '@/app/constants/states'; + +export const runtime = 'edge' + +interface BillData { + bill_number: string; + title: string; + description: string; + state_abbr: string; + state_name: string; + status_desc: string; + current_body_name: string; +} + +async function getBill(billId: string): Promise { + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' + const response = await fetch(`${baseUrl}/api/bills/${billId}`) + if (!response.ok) { + throw new Error('Failed to fetch bill data') + } + return response.json() +} + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url) + const stateCode = searchParams.get('state')?.toUpperCase() + const billId = searchParams.get('id') + + // Handle missing parameters + if (!stateCode || !billId) { + return new Response('Missing required parameters', { status: 400 }) + } + + // Get bill data + const bill = await getBill(billId) + if (!bill) { + return new Response('Bill not found', { status: 404 }) + } + + // Get state name + const stateName = STATE_NAMES[stateCode] || stateCode + + // Format title + const title = bill.title !== bill.description + ? `${bill.bill_number}: ${bill.title}` + : bill.bill_number + + // Truncate description if needed + const description = bill.description.length > 120 + ? bill.description.substring(0, 117) + '...' + : bill.description + + return new ImageResponse( + ( +
+
+ {/* State SVG */} + {stateName} +
+ + + + + LegiEquity + +
+
+
+ {title} +
+
+ {description} +
+
+ {stateName} Legislature • {bill.current_body_name} • {bill.status_desc} +
+
+ ), + { + width: 1200, + height: 630, + }, + ) + } catch (e: unknown) { + console.log(`${e instanceof Error ? e.message : 'Unknown error'}`) + return new Response(`Failed to generate the image`, { + status: 500, + }) + } +} \ No newline at end of file diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx index 93c7759..faa7998 100644 --- a/app/api/og/route.tsx +++ b/app/api/og/route.tsx @@ -165,8 +165,8 @@ export async function GET(req: NextRequest) { height: 630, }, ) - } catch (e: any) { - console.log(`${e.message}`) + } catch (e: unknown) { + console.log(`${e instanceof Error ? e.message : 'Unknown error'}`) return new Response(`Failed to generate the image`, { status: 500, }) diff --git a/app/constants/states.ts b/app/constants/states.ts new file mode 100644 index 0000000..4fe2b2b --- /dev/null +++ b/app/constants/states.ts @@ -0,0 +1,59 @@ +export const STATE_NAMES: { [key: string]: string } = { + // Federal + 'US': 'United States Congress', + + // States + 'AL': 'Alabama', + 'AK': 'Alaska', + 'AZ': 'Arizona', + 'AR': 'Arkansas', + 'CA': 'California', + 'CO': 'Colorado', + 'CT': 'Connecticut', + 'DE': 'Delaware', + 'FL': 'Florida', + 'GA': 'Georgia', + 'HI': 'Hawaii', + 'ID': 'Idaho', + 'IL': 'Illinois', + 'IN': 'Indiana', + 'IA': 'Iowa', + 'KS': 'Kansas', + 'KY': 'Kentucky', + 'LA': 'Louisiana', + 'ME': 'Maine', + 'MD': 'Maryland', + 'MA': 'Massachusetts', + 'MI': 'Michigan', + 'MN': 'Minnesota', + 'MS': 'Mississippi', + 'MO': 'Missouri', + 'MT': 'Montana', + 'NE': 'Nebraska', + 'NV': 'Nevada', + 'NH': 'New Hampshire', + 'NJ': 'New Jersey', + 'NM': 'New Mexico', + 'NY': 'New York', + 'NC': 'North Carolina', + 'ND': 'North Dakota', + 'OH': 'Ohio', + 'OK': 'Oklahoma', + 'OR': 'Oregon', + 'PA': 'Pennsylvania', + 'RI': 'Rhode Island', + 'SC': 'South Carolina', + 'SD': 'South Dakota', + 'TN': 'Tennessee', + 'TX': 'Texas', + 'UT': 'Utah', + 'VT': 'Vermont', + 'VA': 'Virginia', + 'WA': 'Washington', + 'WV': 'West Virginia', + 'WI': 'Wisconsin', + 'WY': 'Wyoming', + + // District of Columbia + 'DC': 'District of Columbia' +} as const; \ No newline at end of file diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx deleted file mode 100644 index 0519ecb..0000000 --- a/components/ui/dialog.tsx +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From e9fa738219f886318c7c6b2c624e69f663c34579 Mon Sep 17 00:00:00 2001 From: dukesvrtech Date: Fri, 31 Jan 2025 13:44:48 -0500 Subject: [PATCH 07/18] Added OG to Sponsors --- app/api/bills/[id]/route.ts | 31 +++++++ app/api/og/sponsor/route.tsx | 151 +++++++++++++++++++++++++++++++++ app/api/sponsors/[id]/route.ts | 49 +++++++++++ app/sponsor/[id]/page.tsx | 34 ++++++++ 4 files changed, 265 insertions(+) create mode 100644 app/api/bills/[id]/route.ts create mode 100644 app/api/og/sponsor/route.tsx create mode 100644 app/api/sponsors/[id]/route.ts diff --git a/app/api/bills/[id]/route.ts b/app/api/bills/[id]/route.ts new file mode 100644 index 0000000..8dff886 --- /dev/null +++ b/app/api/bills/[id]/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from 'next/server' +import db from "@/lib/db"; + +export async function GET( + request: Request, + { params }: { params: { id: string } } +) { + try { + const [bill] = await db` + SELECT + bill_number, + title, + description, + state_abbr, + state_name, + status_desc, + current_body_name + FROM lsv_bill + WHERE bill_id = ${params.id} + ` + + if (!bill) { + return new NextResponse('Bill not found', { status: 404 }) + } + + return NextResponse.json(bill) + } catch (error) { + console.error('Error fetching bill:', error) + return new NextResponse('Internal Server Error', { status: 500 }) + } +} \ No newline at end of file diff --git a/app/api/og/sponsor/route.tsx b/app/api/og/sponsor/route.tsx new file mode 100644 index 0000000..97dc441 --- /dev/null +++ b/app/api/og/sponsor/route.tsx @@ -0,0 +1,151 @@ +import { ImageResponse } from '@vercel/og' +import { NextRequest } from 'next/server' +import { STATE_NAMES } from '@/app/constants/states'; + +export const runtime = 'edge' + +interface SponsorData { + name: string; + title: string; + state_abbr: string; + party: string; + district?: string; + bills_sponsored: number; + bills_cosponsored: number; + votesmart_id?: string | null; +} + +async function getSponsor(sponsorId: string): Promise { + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' + const response = await fetch(`${baseUrl}/api/sponsors/${sponsorId}`) + if (!response.ok) { + throw new Error('Failed to fetch sponsor data') + } + return response.json() +} + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url) + const stateCode = searchParams.get('state')?.toUpperCase() + const sponsorId = searchParams.get('id') + + // Handle missing parameters + if (!stateCode || !sponsorId) { + return new Response('Missing required parameters', { status: 400 }) + } + + // Get sponsor data + const sponsor = await getSponsor(sponsorId) + if (!sponsor) { + return new Response('Sponsor not found', { status: 404 }) + } + + // Get state name + const stateName = STATE_NAMES[stateCode] || stateCode + + // Format district/title + const roleText = sponsor.district + ? `${sponsor.party}-${sponsor.district}` + : sponsor.title + + return new ImageResponse( + ( +
+ {/* Header with photo and logo */} +
+ {sponsor.votesmart_id ? ( + {sponsor.name} + ) : ( + {stateName} + )} +
+ + + + + LegiEquity + +
+
+ + {/* Content */} +
+ + {sponsor.name} + + + {stateName} • {roleText} + + + {sponsor.bills_sponsored} Bills Sponsored • {sponsor.bills_cosponsored} Bills Co-sponsored + +
+
+ ), + { + width: 1200, + height: 630, + }, + ) + } catch (e: unknown) { + console.log(`${e instanceof Error ? e.message : 'Unknown error'}`) + return new Response(`Failed to generate the image`, { + status: 500, + }) + } +} \ No newline at end of file diff --git a/app/api/sponsors/[id]/route.ts b/app/api/sponsors/[id]/route.ts new file mode 100644 index 0000000..67e7fd9 --- /dev/null +++ b/app/api/sponsors/[id]/route.ts @@ -0,0 +1,49 @@ +import { NextResponse } from 'next/server' +import db from "@/lib/db"; + +export async function GET( + request: Request, + { params }: { params: { id: string } } +) { + try { + const [sponsor] = await db` + SELECT + p.name, + r.role_name as title, + st.state_abbr, + pa.party_name as party, + p.district, + p.votesmart_id, + ( + SELECT COUNT(*)::int + FROM ls_bill_sponsor bs + INNER JOIN ls_bill b ON bs.bill_id = b.bill_id + WHERE bs.people_id = p.people_id + AND bs.sponsor_type_id = 1 + AND b.bill_type_id = 1 + ) as bills_sponsored, + ( + SELECT COUNT(*)::int + FROM ls_bill_sponsor bs + INNER JOIN ls_bill b ON bs.bill_id = b.bill_id + WHERE bs.people_id = p.people_id + AND bs.sponsor_type_id = 2 + AND b.bill_type_id = 1 + ) as bills_cosponsored + FROM ls_people p + INNER JOIN ls_party pa ON p.party_id = pa.party_id + INNER JOIN ls_role r ON p.role_id = r.role_id + INNER JOIN ls_state st ON p.state_id = st.state_id + WHERE p.people_id = ${params.id} + ` + + if (!sponsor) { + return new NextResponse('Sponsor not found', { status: 404 }) + } + + return NextResponse.json(sponsor) + } catch (error) { + console.error('Error fetching sponsor:', error) + return new NextResponse('Internal Server Error', { status: 500 }) + } +} \ No newline at end of file diff --git a/app/sponsor/[id]/page.tsx b/app/sponsor/[id]/page.tsx index ec272d8..ddf57b4 100644 --- a/app/sponsor/[id]/page.tsx +++ b/app/sponsor/[id]/page.tsx @@ -9,6 +9,7 @@ import { SubgroupBarChart } from '@/app/components/analytics/SubgroupBarChart'; import { VotingHistory } from '@/app/components/sponsor/VotingHistory'; import { Footer } from "@/app/components/layout/Footer"; import { SponsoredBillsList } from '@/app/components/sponsor/SponsoredBillsList'; +import { Metadata } from 'next'; interface SubgroupScore { subgroup_code: string; @@ -500,4 +501,37 @@ export default async function SponsorPage({
); +} + +interface Props { + params: { id: string } +} + +export async function generateMetadata({ params }: Props): Promise { + const sponsor = await getSponsor(params.id); + if (!sponsor) return {}; + + const title = `${sponsor.name} - Legislative Analysis | LegiEquity`; + const description = `Analyzing the demographic impact of legislation sponsored by ${sponsor.name}, ${sponsor.role_name} from ${sponsor.state_name}`; + + return { + title, + description, + openGraph: { + title, + description, + images: [{ + url: `/api/og/sponsor?id=${params.id}&state=${sponsor.state_abbr}`, + width: 1200, + height: 630, + alt: title + }] + }, + twitter: { + card: 'summary_large_image', + title, + description, + images: [`/api/og/sponsor?id=${params.id}&state=${sponsor.state_abbr}`], + } + }; } \ No newline at end of file From 2874d8a33277f590c6d182f33ac1fd467681bc7f Mon Sep 17 00:00:00 2001 From: dukesvrtech Date: Fri, 31 Jan 2025 13:55:28 -0500 Subject: [PATCH 08/18] Fixed issues with OG image generation --- app/api/og/bill/route.tsx | 51 ++++++++++++++++++------- app/api/og/route.tsx | 79 ++++++++++++++++++++++++++------------- 2 files changed, 92 insertions(+), 38 deletions(-) diff --git a/app/api/og/bill/route.tsx b/app/api/og/bill/route.tsx index 3a0c3b6..cb5bbb6 100644 --- a/app/api/og/bill/route.tsx +++ b/app/api/og/bill/route.tsx @@ -65,41 +65,66 @@ export async function GET(req: NextRequest) { justifyContent: 'center', backgroundColor: '#030712', padding: '40px 80px', + gap: '40px', }} > + {/* Header with state icon and logo */}
- {/* State SVG */} {stateName} -
+
- + LegiEquity
-
- {title} -
-
- {description} -
-
- {stateName} Legislature • {bill.current_body_name} • {bill.status_desc} + + {/* Content */} +
+ + {title} + + + {description} + + + {stateName} Legislature • {bill.current_body_name} • {bill.status_desc} +
), diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx index faa7998..cce78ba 100644 --- a/app/api/og/route.tsx +++ b/app/api/og/route.tsx @@ -23,6 +23,7 @@ export async function GET(req: NextRequest) { justifyContent: 'center', backgroundColor: '#030712', padding: '40px 80px', + gap: '40px', }} >
- + LegiEquity
-
+ AI-powered analysis of legislation's impact on demographic equity -
+
), { @@ -69,6 +70,7 @@ export async function GET(req: NextRequest) { justifyContent: 'center', backgroundColor: '#030712', padding: '40px 80px', + gap: '40px', }} >
- {/* US Congress Seal */} US Congress -
+
- + LegiEquity
-
- Congressional Legislative Analysis -
-
- Analyzing demographic impact of federal legislation across age, disability, gender, race, and religion +
+ + Congressional Legislative Analysis + + + Analyzing demographic impact of federal legislation across age, disability, gender, race, and religion +
), @@ -112,7 +127,7 @@ export async function GET(req: NextRequest) { ) } - // State-specific image with SVG + // State-specific image return new ImageResponse( (
- {/* State SVG */} {stateName} -
+
- + LegiEquity
-
- {stateName} Legislative Analysis -
-
- Analyzing demographic impact of legislation across age, disability, gender, race, and religion +
+ + {stateName} Legislative Analysis + + + Analyzing demographic impact of legislation across age, disability, gender, race, and religion +
), From 47fc6a9f30ded3c9e933d101f8c722268f6ae421 Mon Sep 17 00:00:00 2001 From: dukesvrtech Date: Fri, 31 Jan 2025 13:59:48 -0500 Subject: [PATCH 09/18] Finished Opened Graph --- app/about/page.tsx | 22 ++++++++++++++++++++++ app/blog/page.tsx | 22 ++++++++++++++++++++++ app/contact/page.tsx | 22 ++++++++++++++++++++++ app/terms/page.tsx | 22 ++++++++++++++++++++++ 4 files changed, 88 insertions(+) diff --git a/app/about/page.tsx b/app/about/page.tsx index 5529d5b..52ae8c9 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -1,5 +1,27 @@ import { Footer } from "@/app/components/layout/Footer"; import { AuroraBackground } from "@/app/components/ui/aurora-background"; +import { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'About LegiEquity', + description: 'Learn about our mission to analyze legislative impact on demographic equity using AI', + openGraph: { + title: 'About LegiEquity', + description: 'Learn about our mission to analyze legislative impact on demographic equity using AI', + images: [{ + url: '/api/og/static?page=about', + width: 1200, + height: 630, + alt: 'About LegiEquity' + }] + }, + twitter: { + card: 'summary_large_image', + title: 'About LegiEquity', + description: 'Learn about our mission to analyze legislative impact on demographic equity using AI', + images: ['/api/og/static?page=about'], + } +} export default function AboutPage() { return ( diff --git a/app/blog/page.tsx b/app/blog/page.tsx index 97284c7..cdb7cbf 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -1,6 +1,28 @@ import { Newspaper, Sparkles, Scale, Users, Brain, ArrowRight } from 'lucide-react' import { AuroraBackground } from "@/app/components/ui/aurora-background" import { Footer } from "@/app/components/layout/Footer" +import { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'LegiEquity Blog', + description: 'Insights and updates on legislative analysis and demographic equity', + openGraph: { + title: 'LegiEquity Blog', + description: 'Insights and updates on legislative analysis and demographic equity', + images: [{ + url: '/api/og/static?page=blog', + width: 1200, + height: 630, + alt: 'LegiEquity Blog' + }] + }, + twitter: { + card: 'summary_large_image', + title: 'LegiEquity Blog', + description: 'Insights and updates on legislative analysis and demographic equity', + images: ['/api/og/static?page=blog'], + } +} export default function BlogPage() { return ( diff --git a/app/contact/page.tsx b/app/contact/page.tsx index 640d201..15badda 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -1,6 +1,28 @@ import { Mail, Newspaper, ArrowUpRight } from 'lucide-react' import { AuroraBackground } from "@/app/components/ui/aurora-background" import { Footer } from "@/app/components/layout/Footer" +import { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'Contact Us | LegiEquity', + description: 'Get in touch with the LegiEquity team', + openGraph: { + title: 'Contact Us | LegiEquity', + description: 'Get in touch with the LegiEquity team', + images: [{ + url: '/api/og/static?page=contact', + width: 1200, + height: 630, + alt: 'Contact LegiEquity' + }] + }, + twitter: { + card: 'summary_large_image', + title: 'Contact Us | LegiEquity', + description: 'Get in touch with the LegiEquity team', + images: ['/api/og/static?page=contact'], + } +} export default function ContactPage() { return ( diff --git a/app/terms/page.tsx b/app/terms/page.tsx index 32309b1..6369cbd 100644 --- a/app/terms/page.tsx +++ b/app/terms/page.tsx @@ -1,5 +1,27 @@ import { Footer } from "@/app/components/layout/Footer"; import { AuroraBackground } from "@/app/components/ui/aurora-background"; +import { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'Terms & Agreements | LegiEquity', + description: 'Terms of service, privacy policy, and user agreements', + openGraph: { + title: 'Terms & Agreements | LegiEquity', + description: 'Terms of service, privacy policy, and user agreements', + images: [{ + url: '/api/og/static?page=terms', + width: 1200, + height: 630, + alt: 'Terms & Agreements' + }] + }, + twitter: { + card: 'summary_large_image', + title: 'Terms & Agreements | LegiEquity', + description: 'Terms of service, privacy policy, and user agreements', + images: ['/api/og/static?page=terms'], + } +} export default function TermsPage() { return ( From 679174c3aedc2546b5a54198cec39380ad9ed326 Mon Sep 17 00:00:00 2001 From: dukesvrtech Date: Fri, 31 Jan 2025 14:31:03 -0500 Subject: [PATCH 10/18] Google Analytics included --- app/layout.tsx | 2 ++ package-lock.json | 20 ++++++++++++++++++++ package.json | 1 + 3 files changed, 23 insertions(+) diff --git a/app/layout.tsx b/app/layout.tsx index 38d1897..67e4b62 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import { Inter } from 'next/font/google' import Header from '@/app/components/Header' import { systemThemeScript } from './utils/theme-script' import ClientLayout from './components/ClientLayout' +import { GoogleAnalytics } from '@next/third-parties/google' const inter = Inter({ subsets: ['latin'] }) @@ -29,6 +30,7 @@ export default function RootLayout({ {children} + ) diff --git a/package-lock.json b/package-lock.json index 25ceb94..d7c230c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.4", "dependencies": { "@huggingface/transformers": "^3.3.2", + "@next/third-parties": "^15.1.6", "@radix-ui/react-dialog": "^1.1.5", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-select": "^2.1.5", @@ -889,6 +890,19 @@ "node": ">= 10" } }, + "node_modules/@next/third-parties": { + "version": "15.1.6", + "resolved": "https://registry.npmjs.org/@next/third-parties/-/third-parties-15.1.6.tgz", + "integrity": "sha512-F0uemUqFwD3lLx5SrWXYRe9dZvMVkO0rFuMnvLiPBcagxNc23Ufl5cNXEm4Yuo8O1Mu8dgh+VjExMz1Td4vBew==", + "license": "MIT", + "dependencies": { + "third-party-capital": "1.0.20" + }, + "peerDependencies": { + "next": "^13.0.0 || ^14.0.0 || ^15.0.0", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -7159,6 +7173,12 @@ "node": ">=0.8" } }, + "node_modules/third-party-capital": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/third-party-capital/-/third-party-capital-1.0.20.tgz", + "integrity": "sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==", + "license": "ISC" + }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", diff --git a/package.json b/package.json index bf870da..f33e91e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@huggingface/transformers": "^3.3.2", + "@next/third-parties": "^15.1.6", "@radix-ui/react-dialog": "^1.1.5", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-select": "^2.1.5", From 9fd19e9ef3e2c59de76982d022dc166efc88a62c Mon Sep 17 00:00:00 2001 From: dukesvrtech Date: Fri, 31 Jan 2025 15:26:55 -0500 Subject: [PATCH 11/18] Google data-track- tag for simple tracking --- app/api/og/static/route.tsx | 89 ++++++++++ app/hooks/useAnalytics.ts | 168 ++++++++++++++++++ app/layout.tsx | 9 +- app/providers/AnalyticsProvider.tsx | 10 ++ app/utils/analytics.ts | 42 +++++ .../google-analytics-tracking.md | 139 +++++++++++++++ 6 files changed, 454 insertions(+), 3 deletions(-) create mode 100644 app/api/og/static/route.tsx create mode 100644 app/hooks/useAnalytics.ts create mode 100644 app/providers/AnalyticsProvider.tsx create mode 100644 app/utils/analytics.ts create mode 100644 bill-analysis-design/google-analytics-tracking.md diff --git a/app/api/og/static/route.tsx b/app/api/og/static/route.tsx new file mode 100644 index 0000000..c1846a0 --- /dev/null +++ b/app/api/og/static/route.tsx @@ -0,0 +1,89 @@ +import { ImageResponse } from '@vercel/og' +import { NextRequest } from 'next/server' + +export const runtime = 'edge' + +const TITLES = { + about: 'About LegiEquity', + terms: 'Terms & Agreements', + blog: 'LegiEquity Blog', + contact: 'Contact Us' +} as const; + +const DESCRIPTIONS = { + about: 'Learn about our mission to analyze legislative impact on demographic equity using AI', + terms: 'Terms of service, privacy policy, and user agreements', + blog: 'Insights and updates on legislative analysis and demographic equity', + contact: 'Get in touch with the LegiEquity team' +} as const; + +type PageType = keyof typeof TITLES; + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url) + const page = searchParams.get('page')?.toLowerCase() as PageType + + if (!page || !TITLES[page]) { + return new Response('Invalid page parameter', { status: 400 }) + } + + return new ImageResponse( + ( +
+
+ + + + + LegiEquity + +
+
+ + {TITLES[page]} + + + {DESCRIPTIONS[page]} + +
+
+ ), + { + width: 1200, + height: 630, + }, + ) + } catch (e: unknown) { + console.log(`${e instanceof Error ? e.message : 'Unknown error'}`) + return new Response(`Failed to generate the image`, { + status: 500, + }) + } +} \ No newline at end of file diff --git a/app/hooks/useAnalytics.ts b/app/hooks/useAnalytics.ts new file mode 100644 index 0000000..d247700 --- /dev/null +++ b/app/hooks/useAnalytics.ts @@ -0,0 +1,168 @@ +'use client' + +import { useEffect, useCallback } from 'react' +import { usePathname, useSearchParams } from 'next/navigation' +import { safeGtag, initializeGtag } from '@/app/utils/analytics' + +declare global { + interface Window { + gtag: ( + type: string, + action: string, + data?: { [key: string]: any } + ) => void + } +} + +type TrackingAttributes = { + 'data-track-click'?: string; + 'data-track-inview'?: string; + 'data-track-event-category'?: string; + 'data-track-event-action'?: string; + 'data-track-event-label'?: string; + 'data-track-event-value'?: string; + 'data-track-custom-props'?: string; +} + +export const useAnalytics = () => { + const pathname = usePathname() + const searchParams = useSearchParams() + + // Initialize GA + useEffect(() => { + initializeGtag() + }, []) + + // Track page views + useEffect(() => { + if (pathname) { + safeGtag('event', 'page_view', { + page_path: pathname, + page_search: searchParams?.toString(), + page_title: document.title + }) + } + }, [pathname, searchParams]) + + // Core tracking function + const trackEvent = useCallback((action: string, data?: { [key: string]: any }) => { + try { + safeGtag('event', action, { + ...data, + page_path: pathname, + page_title: document.title + }) + } catch (error) { + console.error('Analytics Error:', error) + } + }, [pathname]) + + // Extract tracking data from element + const getTrackingData = (element: HTMLElement & Partial) => { + const category = element.getAttribute('data-track-event-category') + const action = element.getAttribute('data-track-event-action') + const label = element.getAttribute('data-track-event-label') + const value = element.getAttribute('data-track-event-value') + const customPropsString = element.getAttribute('data-track-custom-props') + const customProps = customPropsString ? JSON.parse(customPropsString) : {} + + return { + event_category: category, + event_action: action || 'click', + event_label: label, + event_value: value ? parseInt(value, 10) : undefined, + element_type: element.tagName.toLowerCase(), + element_id: element.id || undefined, + element_class: element.className || undefined, + ...customProps + } + } + + // Click tracking handler + const handleClick = useCallback((event: MouseEvent) => { + const target = event.target as HTMLElement + const trackElement = target.closest('[data-track-click]') + + if (trackElement) { + const trackingData = getTrackingData(trackElement) + trackEvent(trackingData.event_action, trackingData) + } + }, [trackEvent]) + + // Intersection Observer setup + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const element = entry.target as HTMLElement + const trackingData = getTrackingData(element) + + trackEvent(trackingData.event_action || 'impression', { + ...trackingData, + visibility_percent: Math.round(entry.intersectionRatio * 100) + }) + + // Only track once + observer.unobserve(element) + } + }) + }, + { threshold: [0, 0.5, 1.0] } // Track at 0%, 50%, and 100% visibility + ) + + // Setup observers + const setupObservers = () => { + document.querySelectorAll('[data-track-inview]') + .forEach(element => observer.observe(element)) + } + + // Initial setup + setupObservers() + + // Setup click tracking + document.addEventListener('click', handleClick) + + // Cleanup + return () => { + observer.disconnect() + document.removeEventListener('click', handleClick) + } + }, [trackEvent, handleClick]) + + return { trackEvent } +} + +// Example usage in JSX: +/* + // Click tracking + + + // Impression tracking +
+ Content +
+ + // Manual tracking + const { trackEvent } = useAnalytics() + trackEvent('custom_event', { + event_category: 'User Action', + event_label: 'Custom Interaction', + custom_prop: 'value' + }) +*/ \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 67e4b62..f99c669 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,7 @@ import Header from '@/app/components/Header' import { systemThemeScript } from './utils/theme-script' import ClientLayout from './components/ClientLayout' import { GoogleAnalytics } from '@next/third-parties/google' +import { AnalyticsProvider } from './providers/AnalyticsProvider' const inter = Inter({ subsets: ['latin'] }) @@ -26,9 +27,11 @@ export default function RootLayout({
- - {children} - + + + {children} + +
diff --git a/app/providers/AnalyticsProvider.tsx b/app/providers/AnalyticsProvider.tsx new file mode 100644 index 0000000..ec7e883 --- /dev/null +++ b/app/providers/AnalyticsProvider.tsx @@ -0,0 +1,10 @@ +'use client' + +import { useAnalytics } from '@/app/hooks/useAnalytics' + +export function AnalyticsProvider({ children }: { children: React.ReactNode }) { + // Initialize analytics hook + useAnalytics() + + return <>{children} +} \ No newline at end of file diff --git a/app/utils/analytics.ts b/app/utils/analytics.ts new file mode 100644 index 0000000..649a5a7 --- /dev/null +++ b/app/utils/analytics.ts @@ -0,0 +1,42 @@ +'use client' + +declare global { + interface Window { + gtag: ( + type: string, + action: string, + data?: { [key: string]: any } + ) => void + dataLayer: IArguments[] + } +} + +export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID + +// Initialize gtag +export const initializeGtag = () => { + if (typeof window !== 'undefined') { + window.dataLayer = window.dataLayer || [] + function gtag(...args: any[]) { + window.dataLayer.push(args) + } + window.gtag = gtag + window.gtag('js', new Date().toISOString()) + window.gtag('config', GA_MEASUREMENT_ID!, { + page_path: window.location.pathname, + }) + } +} + +// Safe gtag call +export const safeGtag = ( + type: string, + action: string, + data?: { [key: string]: any } +) => { + if (typeof window !== 'undefined' && window.gtag) { + window.gtag(type, action, data) + } else { + console.warn('GTM not initialized') + } +} \ No newline at end of file diff --git a/bill-analysis-design/google-analytics-tracking.md b/bill-analysis-design/google-analytics-tracking.md new file mode 100644 index 0000000..2f34d79 --- /dev/null +++ b/bill-analysis-design/google-analytics-tracking.md @@ -0,0 +1,139 @@ +# Google Analytics (GA4) Implementation Guide + +## Overview +This document outlines the Google Analytics 4 (GA4) implementation in the LegiEquity Monitor application. The implementation uses Next.js's `@next/third-parties` package for the base setup and includes custom tracking capabilities through data attributes and a custom hook. + +## Key Files +1. `app/layout.tsx` - Root layout with GA4 initialization +2. `app/utils/analytics.ts` - Core analytics utilities and initialization +3. `app/hooks/useAnalytics.ts` - Custom analytics hook implementation (Client Component) +4. `app/providers/AnalyticsProvider.tsx` - Analytics context provider (Client Component) + +## Important Notes +- Both `useAnalytics.ts` and `AnalyticsProvider.tsx` are marked with `'use client'` as they use client-side features +- The analytics hook uses browser APIs and React hooks that are only available in Client Components +- The provider should wrap any components that need analytics tracking capabilities +- The `analytics.ts` utility provides safe initialization and event tracking + +## Implementation Details + +### Analytics Initialization +The analytics implementation follows these steps: +1. GA4 script is loaded via `@next/third-parties/google` in the root layout +2. `analytics.ts` initializes the gtag function and dataLayer +3. `useAnalytics` hook sets up event listeners and tracking functionality +4. `AnalyticsProvider` wraps the application to provide analytics context + +## Tracking Methods + +### 1. Automatic Page View Tracking +Page views are automatically tracked on route changes. The following data is captured: +- `page_path`: Current URL path +- `page_search`: URL search parameters +- `page_title`: Document title + +### 2. Click Tracking +Elements can be tracked on click using data attributes: + +```jsx + +``` + +### 3. Impression (In-View) Tracking +Elements can be tracked when they come into view: + +```jsx +
+ Content +
+``` + +### 4. Manual Event Tracking +Events can be tracked programmatically using the `trackEvent` function: + +```typescript +const { trackEvent } = useAnalytics() +trackEvent('custom_event', { + event_category: 'User Action', + event_label: 'Custom Interaction', + custom_prop: 'value' +}) +``` + +## Data Attributes Reference + +| Attribute | Description | Example Value | +|-----------|-------------|---------------| +| `data-track-click` | Enables click tracking | No value needed | +| `data-track-inview` | Enables impression tracking | No value needed | +| `data-track-event-category` | Event category | "Navigation", "Content", "User Action" | +| `data-track-event-action` | Event action | "button_click", "section_view" | +| `data-track-event-label` | Event label | "Submit Form", "Hero Section" | +| `data-track-event-value` | Numeric value | "1", "100" | +| `data-track-custom-props` | JSON string of custom properties | '{"button_type": "submit"}' | + +## Tracked Properties +All events automatically include: +- `page_path`: Current URL path +- `page_title`: Document title +- `element_type`: HTML tag name (for click/impression events) +- `element_id`: Element ID if present +- `element_class`: Element classes if present +- `visibility_percent`: Visibility percentage (for impression events) + +## Implementation Guidelines + +1. **Categories**: Use consistent categories across similar elements: + - Navigation + - Content + - User Action + - Form + - Bill + - Search + - Filter + +2. **Actions**: Use descriptive verbs in snake_case: + - `button_click` + - `section_view` + - `form_submit` + - `bill_view` + - `search_execute` + - `filter_apply` + +3. **Labels**: Use specific, descriptive labels that identify the element: + - "Submit Form" + - "Hero Section" + - "Bill Details" + - "Search Results" + +4. **Custom Properties**: Include additional context when relevant: + ```json + { + "button_type": "submit", + "section_type": "hero", + "bill_id": "HB123", + "search_term": "education" + } + ``` + +## Best Practices +1. Always include event category, action, and label for better data organization +2. Use consistent naming conventions across similar elements +3. Add custom properties for additional context when needed +4. Test tracking implementation in GA4 debug mode before deployment +5. Document new tracking additions in the codebase \ No newline at end of file From d3e42d30d7f339de9d7ec06e08beea6e024dd3ed Mon Sep 17 00:00:00 2001 From: dukesvrtech Date: Fri, 31 Jan 2025 15:53:28 -0500 Subject: [PATCH 12/18] Google Analytics --- app/components/FilterDrawer.tsx | 154 ++++++++++++++++--------- app/components/search/SearchDialog.tsx | 8 +- app/hooks/useAnalytics.ts | 8 +- app/utils/analytics.ts | 14 ++- 4 files changed, 123 insertions(+), 61 deletions(-) diff --git a/app/components/FilterDrawer.tsx b/app/components/FilterDrawer.tsx index d98f4ec..e61989a 100644 --- a/app/components/FilterDrawer.tsx +++ b/app/components/FilterDrawer.tsx @@ -2,7 +2,7 @@ import { useRouter, useSearchParams, usePathname } from 'next/navigation'; import { useEffect, useState } from 'react'; -import { SlidersHorizontal } from "lucide-react"; +import { Filter } from "lucide-react"; import { Sheet, SheetContent, @@ -12,6 +12,7 @@ import { SheetDescription, } from "@/components/ui/sheet"; import { Button } from "@/app/components/ui/button"; +import { useAnalytics } from '@/app/hooks/useAnalytics'; const RACE_CODES = { AI: 'American Indian/Alaska Native', @@ -28,10 +29,21 @@ interface FilterOptions { categories: string[]; } +interface FilterEventData { + event_category: string; + event_label: string; + filter_categories?: string[]; + filter_race_code?: string | null; + filter_impact_type?: string | null; + filter_severity?: string | null; + filter_committee?: string | null; +} + export default function FilterDrawer() { const router = useRouter(); const searchParams = useSearchParams(); const pathname = usePathname(); + const { trackEvent } = useAnalytics(); const [options, setOptions] = useState({ committees: [], @@ -55,11 +67,89 @@ export default function FilterDrawer() { fetchOptions(); }, []); + const handleFilterApply = (params: URLSearchParams) => { + // Track the filter application + trackEvent('filter_apply', { + event_category: 'Filter', + event_label: 'Bills Filter', + filter_categories: params.getAll('categories'), + filter_race_code: params.get('race_code'), + filter_impact_type: params.get('impact_type'), + filter_severity: params.get('severity'), + filter_committee: params.get('committee') + } as FilterEventData); + + // Apply the filter + router.push(`${pathname}?${params.toString()}`); + }; + + // Update the category checkbox handler + const handleCategoryChange = (category: string, checked: boolean) => { + const params = new URLSearchParams(searchParams.toString()); + if (checked) { + params.append('categories', category); + } else { + const values = params.getAll('categories').filter(v => v !== category); + params.delete('categories'); + values.forEach(v => params.append('categories', v)); + } + handleFilterApply(params); + }; + + // Update other filter handlers similarly + const handleRaceCodeChange = (code: string) => { + const params = new URLSearchParams(searchParams.toString()); + if (code) { + params.set('race_code', code); + } else { + params.delete('race_code'); + } + handleFilterApply(params); + }; + + const handleImpactTypeChange = (type: string) => { + const params = new URLSearchParams(searchParams.toString()); + if (type) { + params.set('impact_type', type); + } else { + params.delete('impact_type'); + } + handleFilterApply(params); + }; + + const handleSeverityChange = (severity: string) => { + const params = new URLSearchParams(searchParams.toString()); + if (severity) { + params.set('severity', severity); + } else { + params.delete('severity'); + } + handleFilterApply(params); + }; + + const handleCommitteeChange = (committee: string) => { + const params = new URLSearchParams(searchParams.toString()); + if (committee) { + params.set('committee', committee); + } else { + params.delete('committee'); + } + handleFilterApply(params); + }; + return ( - @@ -82,17 +172,7 @@ export default function FilterDrawer() { { - const params = new URLSearchParams(searchParams.toString()); - if (e.target.checked) { - params.append('categories', category); - } else { - const values = params.getAll('categories').filter(v => v !== category); - params.delete('categories'); - values.forEach(v => params.append('categories', v)); - } - router.push(`${pathname}?${params.toString()}`); - }} + onChange={e => handleCategoryChange(category, e.target.checked)} className="rounded border-zinc-300 dark:border-zinc-600 text-blue-600 focus:ring-blue-500" /> {category} @@ -107,11 +187,7 @@ export default function FilterDrawer() {