From 2cb2eeb7a3f679c622c759e1d153c926b915098d Mon Sep 17 00:00:00 2001 From: ThatXliner Date: Tue, 26 May 2026 23:00:00 -0700 Subject: [PATCH 01/13] feat(expo): add date utilities for election countdown Co-Authored-By: Claude Opus 4.5 --- apps/expo/src/utils/dates.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 apps/expo/src/utils/dates.ts diff --git a/apps/expo/src/utils/dates.ts b/apps/expo/src/utils/dates.ts new file mode 100644 index 0000000..cb2cfb2 --- /dev/null +++ b/apps/expo/src/utils/dates.ts @@ -0,0 +1,21 @@ +export function daysUntil(dateString: string): number { + const target = new Date(dateString); + const now = new Date(); + const diffTime = target.getTime() - now.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays; +} + +export function formatDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + }); +} + +export function isWithinDays(dateString: string, days: number): boolean { + const daysRemaining = daysUntil(dateString); + return daysRemaining > 0 && daysRemaining <= days; +} From b4e5b23553591862c22001d7dc16320ad635ddc7 Mon Sep 17 00:00:00 2001 From: ThatXliner Date: Tue, 26 May 2026 23:00:13 -0700 Subject: [PATCH 02/13] feat(expo): add useUserAddress hook for address persistence Co-Authored-By: Claude Opus 4.5 --- apps/expo/src/hooks/useUserAddress.ts | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 apps/expo/src/hooks/useUserAddress.ts diff --git a/apps/expo/src/hooks/useUserAddress.ts b/apps/expo/src/hooks/useUserAddress.ts new file mode 100644 index 0000000..c28eea3 --- /dev/null +++ b/apps/expo/src/hooks/useUserAddress.ts @@ -0,0 +1,32 @@ +import { useCallback, useEffect, useState } from "react"; +import AsyncStorage from "@react-native-async-storage/async-storage"; + +const ADDRESS_KEY = "user_address"; + +export function useUserAddress() { + const [address, setAddressState] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + AsyncStorage.getItem(ADDRESS_KEY) + .then((value) => { + setAddressState(value); + setIsLoading(false); + }) + .catch(() => { + setIsLoading(false); + }); + }, []); + + const setAddress = useCallback(async (newAddress: string) => { + await AsyncStorage.setItem(ADDRESS_KEY, newAddress); + setAddressState(newAddress); + }, []); + + const clearAddress = useCallback(async () => { + await AsyncStorage.removeItem(ADDRESS_KEY); + setAddressState(null); + }, []); + + return { address, setAddress, clearAddress, isLoading }; +} From ca721b340311b9899e06ca0a408aefe217fad287 Mon Sep 17 00:00:00 2001 From: ThatXliner Date: Tue, 26 May 2026 23:00:30 -0700 Subject: [PATCH 03/13] feat(expo): add ElectionBanner component with countdown Co-Authored-By: Claude Opus 4.5 --- apps/expo/src/components/ElectionBanner.tsx | 102 ++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 apps/expo/src/components/ElectionBanner.tsx diff --git a/apps/expo/src/components/ElectionBanner.tsx b/apps/expo/src/components/ElectionBanner.tsx new file mode 100644 index 0000000..0825af6 --- /dev/null +++ b/apps/expo/src/components/ElectionBanner.tsx @@ -0,0 +1,102 @@ +import { StyleSheet, TouchableOpacity } from "react-native"; +import { FontAwesome } from "@expo/vector-icons"; + +import { Text, View } from "~/components/Themed"; +import { colors, fontBody, fontSize, rd, sp, useTheme } from "~/styles"; + +interface ElectionBannerProps { + daysUntil: number; + electionName: string; + onPress: () => void; + onDismiss: () => void; +} + +export function ElectionBanner({ + daysUntil, + electionName, + onPress, + onDismiss, +}: ElectionBannerProps) { + const { theme } = useTheme(); + + return ( + + + + + + {daysUntil} days until {electionName} + + Know what's on your ballot + + + See My Ballot + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + marginHorizontal: sp.md, + marginBottom: sp.md, + borderRadius: rd.md, + overflow: "hidden", + }, + accent: { + width: 4, + backgroundColor: colors.civicBlue, + }, + content: { + flex: 1, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + padding: sp.md, + }, + textContainer: { + flex: 1, + marginRight: sp.md, + }, + headline: { + fontFamily: fontBody.medium, + fontSize: fontSize.sm, + color: colors.white, + marginBottom: sp.xs, + }, + days: { + fontFamily: fontBody.bold, + fontSize: fontSize.base, + }, + subtext: { + fontFamily: fontBody.regular, + fontSize: fontSize.xs, + color: colors.textMuted, + }, + cta: { + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.white, + paddingVertical: sp.sm, + paddingHorizontal: sp.md, + borderRadius: 9999, + gap: sp.xs, + }, + ctaText: { + fontFamily: fontBody.semibold, + fontSize: fontSize.xs, + color: colors.black, + }, + dismiss: { + position: "absolute", + top: sp.sm, + right: sp.sm, + padding: sp.xs, + }, +}); From 0d8d1754eeced8aa73b0cb161b11e79e5b454ceb Mon Sep 17 00:00:00 2001 From: ThatXliner Date: Tue, 26 May 2026 23:00:54 -0700 Subject: [PATCH 04/13] feat(expo): add MyBallotSection with address input and ballot preview Co-Authored-By: Claude Opus 4.5 --- apps/expo/src/components/MyBallotSection.tsx | 215 +++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 apps/expo/src/components/MyBallotSection.tsx diff --git a/apps/expo/src/components/MyBallotSection.tsx b/apps/expo/src/components/MyBallotSection.tsx new file mode 100644 index 0000000..74e611a --- /dev/null +++ b/apps/expo/src/components/MyBallotSection.tsx @@ -0,0 +1,215 @@ +import { useState } from "react"; +import { + ActivityIndicator, + StyleSheet, + TextInput, + TouchableOpacity, +} from "react-native"; +import { FontAwesome } from "@expo/vector-icons"; + +import { Text, View } from "~/components/Themed"; +import { + colors, + fontBody, + fontEditorial, + fontSize, + rd, + sp, + useTheme, +} from "~/styles"; +import { trpc } from "~/utils/api"; + +interface MyBallotSectionProps { + address: string | null; + onAddressSubmit: (address: string) => void; + onEditAddress: () => void; +} + +export function MyBallotSection({ + address, + onAddressSubmit, + onEditAddress, +}: MyBallotSectionProps) { + const { theme } = useTheme(); + const [inputValue, setInputValue] = useState(""); + + const voterInfoQuery = trpc.civic.getVoterInfo.useQuery( + { address: address ?? "" }, + { enabled: !!address }, + ); + + if (!address) { + return ( + + My Ballot + + We'll show you exactly what's on your ballot + + + + inputValue && onAddressSubmit(inputValue)} + disabled={!inputValue} + > + Look Up + + + + ); + } + + return ( + + + My Ballot + + + Edit + + + {address} + + {voterInfoQuery.isLoading && ( + + )} + + {voterInfoQuery.data?.contests?.map((contest, index) => ( + + + {contest.office ?? contest.referendumTitle ?? "Contest"} + + + {contest.candidates + ? `${contest.candidates.length} candidates` + : contest.referendumTitle + ? "Ballot Measure" + : ""} + + + + ))} + + {voterInfoQuery.data?.contests?.length === 0 && ( + No ballot information available yet + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + marginHorizontal: sp.md, + marginBottom: sp.lg, + padding: sp.md, + borderRadius: rd.md, + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: sp.sm, + }, + sectionTitle: { + fontFamily: fontEditorial.bold, + fontSize: fontSize.lg, + color: colors.white, + marginBottom: sp.xs, + }, + hint: { + fontFamily: fontBody.regular, + fontSize: fontSize.sm, + color: colors.textMuted, + marginBottom: sp.md, + }, + inputRow: { + flexDirection: "row", + gap: sp.sm, + }, + input: { + flex: 1, + fontFamily: fontBody.regular, + fontSize: fontSize.sm, + color: colors.white, + padding: sp.sm, + borderRadius: rd.sm, + }, + button: { + backgroundColor: colors.white, + paddingVertical: sp.sm, + paddingHorizontal: sp.md, + borderRadius: 9999, + justifyContent: "center", + }, + buttonDisabled: { + opacity: 0.5, + }, + buttonText: { + fontFamily: fontBody.semibold, + fontSize: fontSize.sm, + color: colors.black, + }, + editButton: { + flexDirection: "row", + alignItems: "center", + gap: sp.xs, + }, + editText: { + fontFamily: fontBody.medium, + fontSize: fontSize.sm, + color: colors.civicBlue, + }, + address: { + fontFamily: fontBody.regular, + fontSize: fontSize.sm, + color: colors.textMuted, + marginBottom: sp.md, + }, + loader: { + marginVertical: sp.lg, + }, + contestCard: { + flexDirection: "row", + alignItems: "center", + padding: sp.sm, + borderRadius: rd.sm, + marginBottom: sp.sm, + }, + contestTitle: { + flex: 1, + fontFamily: fontBody.medium, + fontSize: fontSize.sm, + color: colors.white, + }, + contestMeta: { + fontFamily: fontBody.regular, + fontSize: fontSize.xs, + color: colors.textMuted, + marginRight: sp.sm, + }, + chevron: { + marginLeft: sp.xs, + }, + noData: { + fontFamily: fontBody.regular, + fontSize: fontSize.sm, + color: colors.textMuted, + textAlign: "center", + marginVertical: sp.lg, + }, +}); From 8f3ed320bfb0b1d1009551f83cfa5f1ab324605a Mon Sep 17 00:00:00 2001 From: ThatXliner Date: Tue, 26 May 2026 23:01:13 -0700 Subject: [PATCH 05/13] feat(expo): add KeyDatesSection with horizontal scroll cards Co-Authored-By: Claude Opus 4.5 --- apps/expo/src/components/KeyDatesSection.tsx | 144 +++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 apps/expo/src/components/KeyDatesSection.tsx diff --git a/apps/expo/src/components/KeyDatesSection.tsx b/apps/expo/src/components/KeyDatesSection.tsx new file mode 100644 index 0000000..449d0df --- /dev/null +++ b/apps/expo/src/components/KeyDatesSection.tsx @@ -0,0 +1,144 @@ +import { ScrollView, StyleSheet } from "react-native"; + +import { Text, View } from "~/components/Themed"; +import { + colors, + fontBody, + fontEditorial, + fontSize, + rd, + sp, + useTheme, +} from "~/styles"; +import { daysUntil, formatDate } from "~/utils/dates"; + +interface KeyDate { + label: string; + date: string; +} + +interface KeyDatesSectionProps { + electionDate: string; +} + +export function KeyDatesSection({ electionDate }: KeyDatesSectionProps) { + const { theme } = useTheme(); + + const electionDateObj = new Date(electionDate); + const registrationDeadline = new Date(electionDateObj); + registrationDeadline.setDate(registrationDeadline.getDate() - 15); + + const earlyVotingStart = new Date(electionDateObj); + earlyVotingStart.setDate(earlyVotingStart.getDate() - 29); + + const dates: KeyDate[] = [ + { + label: "Registration Deadline", + date: registrationDeadline.toISOString().split("T")[0]!, + }, + { + label: "Early Voting Starts", + date: earlyVotingStart.toISOString().split("T")[0]!, + }, + { + label: "Election Day", + date: electionDate, + }, + ]; + + return ( + + Key Dates + + {dates.map((item, index) => { + const days = daysUntil(item.date); + const isPassed = days < 0; + const isNext = + !isPassed && dates.findIndex((d) => daysUntil(d.date) >= 0) === index; + + return ( + + + {item.label} + + + {formatDate(item.date)} + + + {isPassed ? "Passed" : days === 0 ? "Today!" : `in ${days} days`} + + + ); + })} + + + ); +} + +const styles = StyleSheet.create({ + container: { + marginBottom: sp.lg, + }, + sectionTitle: { + fontFamily: fontEditorial.bold, + fontSize: fontSize.lg, + color: colors.white, + marginHorizontal: sp.md, + marginBottom: sp.sm, + }, + scrollContent: { + paddingHorizontal: sp.md, + gap: sp.sm, + }, + card: { + padding: sp.md, + borderRadius: rd.md, + minWidth: 140, + }, + cardHighlight: { + borderWidth: 2, + borderColor: colors.civicBlue, + }, + label: { + fontFamily: fontBody.medium, + fontSize: fontSize.xs, + color: colors.textMuted, + marginBottom: sp.xs, + }, + date: { + fontFamily: fontBody.semibold, + fontSize: fontSize.sm, + color: colors.white, + marginBottom: sp.xs, + }, + countdown: { + fontFamily: fontBody.regular, + fontSize: fontSize.xs, + color: colors.textMuted, + }, + countdownHighlight: { + color: colors.civicBlue, + fontFamily: fontBody.semibold, + }, + textMuted: { + color: colors.textMuted, + opacity: 0.6, + }, +}); From 3460cd75b1ce151bbe6a0a10ec2982596b31f66c Mon Sep 17 00:00:00 2001 From: ThatXliner Date: Tue, 26 May 2026 23:01:32 -0700 Subject: [PATCH 06/13] feat(expo): add BallotMeasuresSection for propositions Co-Authored-By: Claude Opus 4.5 --- .../src/components/BallotMeasuresSection.tsx | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 apps/expo/src/components/BallotMeasuresSection.tsx diff --git a/apps/expo/src/components/BallotMeasuresSection.tsx b/apps/expo/src/components/BallotMeasuresSection.tsx new file mode 100644 index 0000000..6961891 --- /dev/null +++ b/apps/expo/src/components/BallotMeasuresSection.tsx @@ -0,0 +1,141 @@ +import { StyleSheet, TouchableOpacity } from "react-native"; +import { FontAwesome } from "@expo/vector-icons"; + +import type { Contest } from "@acme/api"; + +import { Text, View } from "~/components/Themed"; +import { + colors, + fontBody, + fontEditorial, + fontSize, + rd, + sp, + useTheme, +} from "~/styles"; + +interface BallotMeasuresSectionProps { + measures: Contest[]; + onMeasurePress?: (measure: Contest) => void; +} + +export function BallotMeasuresSection({ + measures, + onMeasurePress, +}: BallotMeasuresSectionProps) { + const { theme } = useTheme(); + + if (measures.length === 0) { + return null; + } + + return ( + + Ballot Measures + {measures.map((measure, index) => ( + onMeasurePress?.(measure)} + activeOpacity={0.8} + > + + MEASURE + + + {measure.referendumTitle ?? "Ballot Measure"} + + {measure.referendumSubtitle && ( + + {measure.referendumSubtitle} + + )} + {(measure.referendumProStatement ?? measure.referendumConStatement) && ( + + {measure.referendumProStatement && ( + + Yes: + {measure.referendumProStatement} + + )} + {measure.referendumConStatement && ( + + No: + {measure.referendumConStatement} + + )} + + )} + + + ))} + + ); +} + +const styles = StyleSheet.create({ + container: { + marginHorizontal: sp.md, + marginBottom: sp.lg, + }, + sectionTitle: { + fontFamily: fontEditorial.bold, + fontSize: fontSize.lg, + color: colors.white, + marginBottom: sp.sm, + }, + card: { + padding: sp.md, + borderRadius: rd.md, + marginBottom: sp.sm, + }, + badge: { + backgroundColor: colors.civicBlue, + alignSelf: "flex-start", + paddingVertical: 2, + paddingHorizontal: sp.sm, + borderRadius: rd.xs, + marginBottom: sp.sm, + }, + badgeText: { + fontFamily: fontBody.semibold, + fontSize: 10, + color: colors.white, + textTransform: "uppercase", + }, + title: { + fontFamily: fontBody.semibold, + fontSize: fontSize.base, + color: colors.white, + marginBottom: sp.xs, + }, + subtitle: { + fontFamily: fontBody.regular, + fontSize: fontSize.sm, + color: colors.textMuted, + marginBottom: sp.sm, + }, + arguments: { + marginTop: sp.xs, + }, + argument: { + fontFamily: fontBody.regular, + fontSize: fontSize.xs, + color: colors.textMuted, + marginBottom: 2, + }, + argumentLabel: { + fontFamily: fontBody.semibold, + color: colors.white, + }, + chevron: { + position: "absolute", + right: sp.md, + top: "50%", + }, +}); From e0cb51f3fe352b029b31b0cff6fd29988f6c5130 Mon Sep 17 00:00:00 2001 From: ThatXliner Date: Tue, 26 May 2026 23:01:50 -0700 Subject: [PATCH 07/13] feat(expo): add CandidatesSection grouped by race Co-Authored-By: Claude Opus 4.5 --- .../expo/src/components/CandidatesSection.tsx | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 apps/expo/src/components/CandidatesSection.tsx diff --git a/apps/expo/src/components/CandidatesSection.tsx b/apps/expo/src/components/CandidatesSection.tsx new file mode 100644 index 0000000..f798993 --- /dev/null +++ b/apps/expo/src/components/CandidatesSection.tsx @@ -0,0 +1,141 @@ +import { StyleSheet, TouchableOpacity } from "react-native"; +import { Image } from "expo-image"; +import { FontAwesome } from "@expo/vector-icons"; + +import type { Contest } from "@acme/api"; + +import { Text, View } from "~/components/Themed"; +import { + colors, + fontBody, + fontEditorial, + fontSize, + rd, + sp, + useTheme, +} from "~/styles"; + +interface CandidatesSectionProps { + contests: Contest[]; + onCandidatePress?: (contest: Contest, candidateIndex: number) => void; +} + +export function CandidatesSection({ + contests, + onCandidatePress, +}: CandidatesSectionProps) { + const { theme } = useTheme(); + + const candidateContests = contests.filter( + (c) => c.candidates && c.candidates.length > 0, + ); + + if (candidateContests.length === 0) { + return null; + } + + return ( + + Candidates + {candidateContests.map((contest, contestIndex) => ( + + {contest.office ?? "Race"} + {contest.candidates?.map((candidate, candidateIndex) => ( + onCandidatePress?.(contest, candidateIndex)} + activeOpacity={0.8} + > + {candidate.photoUrl ? ( + + ) : ( + + + + )} + + {candidate.name} + {candidate.party && ( + {candidate.party} + )} + + + + ))} + + ))} + + ); +} + +const styles = StyleSheet.create({ + container: { + marginHorizontal: sp.md, + marginBottom: sp.lg, + }, + sectionTitle: { + fontFamily: fontEditorial.bold, + fontSize: fontSize.lg, + color: colors.white, + marginBottom: sp.sm, + }, + raceGroup: { + marginBottom: sp.md, + }, + raceTitle: { + fontFamily: fontBody.semibold, + fontSize: fontSize.sm, + color: colors.textMuted, + marginBottom: sp.sm, + textTransform: "uppercase", + letterSpacing: 0.5, + }, + candidateCard: { + flexDirection: "row", + alignItems: "center", + padding: sp.sm, + borderRadius: rd.md, + marginBottom: sp.xs, + }, + photo: { + width: 44, + height: 44, + borderRadius: 22, + marginRight: sp.sm, + }, + photoPlaceholder: { + width: 44, + height: 44, + borderRadius: 22, + marginRight: sp.sm, + justifyContent: "center", + alignItems: "center", + }, + candidateInfo: { + flex: 1, + }, + candidateName: { + fontFamily: fontBody.medium, + fontSize: fontSize.sm, + color: colors.white, + }, + candidateParty: { + fontFamily: fontBody.regular, + fontSize: fontSize.xs, + color: colors.textMuted, + }, +}); From 0587bac1925022d64a8ee95b7867a6b5a6d7a861 Mon Sep 17 00:00:00 2001 From: ThatXliner Date: Tue, 26 May 2026 23:02:15 -0700 Subject: [PATCH 08/13] feat(expo): add LocalBillsSection with Legistar integration Co-Authored-By: Claude Opus 4.5 --- .../expo/src/components/LocalBillsSection.tsx | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 apps/expo/src/components/LocalBillsSection.tsx diff --git a/apps/expo/src/components/LocalBillsSection.tsx b/apps/expo/src/components/LocalBillsSection.tsx new file mode 100644 index 0000000..4e5cba3 --- /dev/null +++ b/apps/expo/src/components/LocalBillsSection.tsx @@ -0,0 +1,152 @@ +import { ActivityIndicator, StyleSheet, TouchableOpacity } from "react-native"; +import { FontAwesome } from "@expo/vector-icons"; +import { useQuery } from "@tanstack/react-query"; + +import { legistar, type LegistarMatter } from "@acme/api"; + +import { Text, View } from "~/components/Themed"; +import { + colors, + fontBody, + fontEditorial, + fontSize, + rd, + sp, + useTheme, +} from "~/styles"; + +interface LocalBillsSectionProps { + onBillPress?: (bill: LegistarMatter) => void; +} + +export function LocalBillsSection({ onBillPress }: LocalBillsSectionProps) { + const { theme } = useTheme(); + + const billsQuery = useQuery({ + queryKey: ["localBills"], + queryFn: async () => { + const [sanjose, santaclara] = await Promise.all([ + legistar.getLegislation("sanjose", {}).catch(() => []), + legistar.getLegislation("santaclara", {}).catch(() => []), + ]); + + const allBills = [ + ...sanjose.map((b) => ({ ...b, jurisdiction: "San Jose" })), + ...santaclara.map((b) => ({ ...b, jurisdiction: "Santa Clara County" })), + ]; + + return allBills + .sort( + (a, b) => + new Date(b.MatterLastModifiedUtc).getTime() - + new Date(a.MatterLastModifiedUtc).getTime(), + ) + .slice(0, 10); + }, + staleTime: 5 * 60 * 1000, + }); + + return ( + + Local Bills + + {billsQuery.isLoading && ( + + )} + + {billsQuery.data?.map((bill, index) => ( + onBillPress?.(bill as LegistarMatter)} + activeOpacity={0.8} + > + + + + + {(bill as LegistarMatter & { jurisdiction: string }).jurisdiction} + + {bill.MatterStatusName} + + + {bill.MatterTitle} + + {bill.MatterFile} + + + + ))} + + {billsQuery.data?.length === 0 && ( + No recent local legislation + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + marginHorizontal: sp.md, + marginBottom: sp.lg, + }, + sectionTitle: { + fontFamily: fontEditorial.bold, + fontSize: fontSize.lg, + color: colors.white, + marginBottom: sp.sm, + }, + loader: { + marginVertical: sp.lg, + }, + card: { + flexDirection: "row", + alignItems: "center", + borderRadius: rd.md, + marginBottom: sp.sm, + overflow: "hidden", + }, + cardAccent: { + width: 4, + alignSelf: "stretch", + backgroundColor: colors.civicBlue, + }, + cardContent: { + flex: 1, + padding: sp.md, + }, + meta: { + flexDirection: "row", + gap: sp.sm, + marginBottom: sp.xs, + }, + jurisdiction: { + fontFamily: fontBody.semibold, + fontSize: 10, + color: colors.civicBlue, + textTransform: "uppercase", + }, + status: { + fontFamily: fontBody.regular, + fontSize: 10, + color: colors.textMuted, + }, + title: { + fontFamily: fontBody.medium, + fontSize: fontSize.sm, + color: colors.white, + marginBottom: sp.xs, + }, + file: { + fontFamily: fontBody.regular, + fontSize: fontSize.xs, + color: colors.textMuted, + }, + noData: { + fontFamily: fontBody.regular, + fontSize: fontSize.sm, + color: colors.textMuted, + textAlign: "center", + marginVertical: sp.lg, + }, +}); From 02e7e58321f7e4290aab18580e734afa25bf603f Mon Sep 17 00:00:00 2001 From: ThatXliner Date: Tue, 26 May 2026 23:02:39 -0700 Subject: [PATCH 09/13] feat(expo): add LocalElectionsScreen with all sections Co-Authored-By: Claude Opus 4.5 --- apps/expo/src/app/local-elections.tsx | 102 ++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 apps/expo/src/app/local-elections.tsx diff --git a/apps/expo/src/app/local-elections.tsx b/apps/expo/src/app/local-elections.tsx new file mode 100644 index 0000000..e587373 --- /dev/null +++ b/apps/expo/src/app/local-elections.tsx @@ -0,0 +1,102 @@ +import { ScrollView, StyleSheet, TouchableOpacity } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useRouter } from "expo-router"; +import { FontAwesome } from "@expo/vector-icons"; + +import type { Contest } from "@acme/api"; + +import { Text, View } from "~/components/Themed"; +import { BallotMeasuresSection } from "~/components/BallotMeasuresSection"; +import { CandidatesSection } from "~/components/CandidatesSection"; +import { KeyDatesSection } from "~/components/KeyDatesSection"; +import { LocalBillsSection } from "~/components/LocalBillsSection"; +import { MyBallotSection } from "~/components/MyBallotSection"; +import { useUserAddress } from "~/hooks/useUserAddress"; +import { colors, fontDisplay, fontSize, sp, useTheme } from "~/styles"; +import { trpc } from "~/utils/api"; + +export default function LocalElectionsScreen() { + const { theme } = useTheme(); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const { address, setAddress, clearAddress } = useUserAddress(); + + const electionsQuery = trpc.civic.getElections.useQuery(); + const upcomingElection = electionsQuery.data?.elections?.[0]; + + const voterInfoQuery = trpc.civic.getVoterInfo.useQuery( + { address: address ?? "" }, + { enabled: !!address }, + ); + + const contests = voterInfoQuery.data?.contests ?? []; + const measures = contests.filter((c: Contest) => c.referendumTitle); + const candidateContests = contests.filter( + (c: Contest) => c.candidates && c.candidates.length > 0, + ); + + return ( + + + router.back()} style={styles.backButton}> + + + Local Elections + + + + + + + {upcomingElection && ( + + )} + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: sp.md, + paddingBottom: sp.md, + }, + backButton: { + padding: sp.sm, + marginLeft: -sp.sm, + }, + title: { + fontFamily: fontDisplay.bold, + fontSize: fontSize.xl, + color: colors.white, + }, + placeholder: { + width: 34, + }, + scroll: { + flex: 1, + }, + scrollContent: { + paddingBottom: sp.xl, + }, +}); From 7a54161d301719ad89f8071d077cdb4518d6a07c Mon Sep 17 00:00:00 2001 From: ThatXliner Date: Tue, 26 May 2026 23:03:10 -0700 Subject: [PATCH 10/13] feat(expo): integrate ElectionBanner into Browse tab Co-Authored-By: Claude Opus 4.5 --- apps/expo/src/app/(tabs)/index.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/expo/src/app/(tabs)/index.tsx b/apps/expo/src/app/(tabs)/index.tsx index bd9ea89..344f280 100644 --- a/apps/expo/src/app/(tabs)/index.tsx +++ b/apps/expo/src/app/(tabs)/index.tsx @@ -16,6 +16,7 @@ import type { VideoPost } from "@acme/api"; import type { Theme } from "~/styles"; import { Text, View } from "~/components/Themed"; +import { ElectionBanner } from "~/components/ElectionBanner"; import { buttons, colors, @@ -31,6 +32,7 @@ import { useTheme, } from "~/styles"; import { trpc } from "~/utils/api"; +import { daysUntil, isWithinDays } from "~/utils/dates"; interface ContentCard { id: string; @@ -199,10 +201,17 @@ const TAB_CONFIG: { key: VideoPost["type"] | "all"; label: string }[] = [ export default function BrowseScreen() { const insets = useSafeAreaInsets(); const { theme } = useTheme(); + const router = useRouter(); const [selectedTab, setSelectedTab] = useState( "all", ); const [searchQuery, setSearchQuery] = useState(""); + const [bannerDismissed, setBannerDismissed] = useState(false); + + const electionsQuery = trpc.civic.getElections.useQuery(); + const upcomingElection = electionsQuery.data?.elections?.find((e) => + isWithinDays(e.electionDay, 30), + ); const { data: content, @@ -265,6 +274,16 @@ export default function BrowseScreen() { ))} + {/* Election banner */} + {upcomingElection && !bannerDismissed && ( + router.push("/local-elections")} + onDismiss={() => setBannerDismissed(true)} + /> + )} + Date: Tue, 26 May 2026 23:10:06 -0700 Subject: [PATCH 11/13] fix: resolve typecheck and lint errors in local elections components - Replace sp.md/lg/sm/xs with numeric sp[4]/[6]/[3]/[2] keys - Add local color constants to components that need them - Use useQuery with queryOptions pattern instead of .useQuery() - Add @react-native-async-storage/async-storage dependency - Fix implicit any types and non-null assertions Co-Authored-By: Claude Opus 4.5 --- apps/expo/package.json | 1 + apps/expo/src/app/(tabs)/index.tsx | 6 +- apps/expo/src/app/local-elections.tsx | 32 +++++---- .../src/components/BallotMeasuresSection.tsx | 43 ++++++------ .../expo/src/components/CandidatesSection.tsx | 33 +++++----- apps/expo/src/components/ElectionBanner.tsx | 42 +++++++----- apps/expo/src/components/KeyDatesSection.tsx | 45 +++++++------ .../expo/src/components/LocalBillsSection.tsx | 55 +++++++++------- apps/expo/src/components/MyBallotSection.tsx | 66 ++++++++++--------- apps/expo/src/hooks/useUserAddress.ts | 2 +- pnpm-lock.yaml | 41 +++++++++++- 11 files changed, 214 insertions(+), 152 deletions(-) diff --git a/apps/expo/package.json b/apps/expo/package.json index 89a724e..fc4ca7b 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -23,6 +23,7 @@ "@expo-google-fonts/inria-serif": "^0.4.1", "@expo/vector-icons": "^14.1.0", "@legendapp/list": "^2.0.19", + "@react-native-async-storage/async-storage": "^3.1.0", "@ronradtke/react-native-markdown-display": "^8.1.0", "@tanstack/react-query": "catalog:", "@trpc/client": "catalog:", diff --git a/apps/expo/src/app/(tabs)/index.tsx b/apps/expo/src/app/(tabs)/index.tsx index 344f280..ae8e0f8 100644 --- a/apps/expo/src/app/(tabs)/index.tsx +++ b/apps/expo/src/app/(tabs)/index.tsx @@ -15,8 +15,8 @@ import Fuse from "fuse.js"; import type { VideoPost } from "@acme/api"; import type { Theme } from "~/styles"; -import { Text, View } from "~/components/Themed"; import { ElectionBanner } from "~/components/ElectionBanner"; +import { Text, View } from "~/components/Themed"; import { buttons, colors, @@ -208,8 +208,8 @@ export default function BrowseScreen() { const [searchQuery, setSearchQuery] = useState(""); const [bannerDismissed, setBannerDismissed] = useState(false); - const electionsQuery = trpc.civic.getElections.useQuery(); - const upcomingElection = electionsQuery.data?.elections?.find((e) => + const electionsQuery = useQuery(trpc.civic.getElections.queryOptions()); + const upcomingElection = electionsQuery.data?.find((e) => isWithinDays(e.electionDay, 30), ); diff --git a/apps/expo/src/app/local-elections.tsx b/apps/expo/src/app/local-elections.tsx index e587373..2d69ef0 100644 --- a/apps/expo/src/app/local-elections.tsx +++ b/apps/expo/src/app/local-elections.tsx @@ -2,15 +2,16 @@ import { ScrollView, StyleSheet, TouchableOpacity } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useRouter } from "expo-router"; import { FontAwesome } from "@expo/vector-icons"; +import { useQuery } from "@tanstack/react-query"; import type { Contest } from "@acme/api"; -import { Text, View } from "~/components/Themed"; import { BallotMeasuresSection } from "~/components/BallotMeasuresSection"; import { CandidatesSection } from "~/components/CandidatesSection"; import { KeyDatesSection } from "~/components/KeyDatesSection"; import { LocalBillsSection } from "~/components/LocalBillsSection"; import { MyBallotSection } from "~/components/MyBallotSection"; +import { Text, View } from "~/components/Themed"; import { useUserAddress } from "~/hooks/useUserAddress"; import { colors, fontDisplay, fontSize, sp, useTheme } from "~/styles"; import { trpc } from "~/utils/api"; @@ -21,13 +22,13 @@ export default function LocalElectionsScreen() { const insets = useSafeAreaInsets(); const { address, setAddress, clearAddress } = useUserAddress(); - const electionsQuery = trpc.civic.getElections.useQuery(); - const upcomingElection = electionsQuery.data?.elections?.[0]; + const electionsQuery = useQuery(trpc.civic.getElections.queryOptions()); + const upcomingElection = electionsQuery.data?.[0]; - const voterInfoQuery = trpc.civic.getVoterInfo.useQuery( - { address: address ?? "" }, - { enabled: !!address }, - ); + const voterInfoQuery = useQuery({ + ...trpc.civic.getVoterInfo.queryOptions({ address: address ?? "" }), + enabled: !!address, + }); const contests = voterInfoQuery.data?.contests ?? []; const measures = contests.filter((c: Contest) => c.referendumTitle); @@ -37,8 +38,11 @@ export default function LocalElectionsScreen() { return ( - - router.back()} style={styles.backButton}> + + router.back()} + style={styles.backButton} + > Local Elections @@ -78,12 +82,12 @@ const styles = StyleSheet.create({ flexDirection: "row", alignItems: "center", justifyContent: "space-between", - paddingHorizontal: sp.md, - paddingBottom: sp.md, + paddingHorizontal: sp[4], + paddingBottom: sp[4], }, backButton: { - padding: sp.sm, - marginLeft: -sp.sm, + padding: sp[3], + marginLeft: -sp[3], }, title: { fontFamily: fontDisplay.bold, @@ -97,6 +101,6 @@ const styles = StyleSheet.create({ flex: 1, }, scrollContent: { - paddingBottom: sp.xl, + paddingBottom: sp[5], }, }); diff --git a/apps/expo/src/components/BallotMeasuresSection.tsx b/apps/expo/src/components/BallotMeasuresSection.tsx index 6961891..22ed6e1 100644 --- a/apps/expo/src/components/BallotMeasuresSection.tsx +++ b/apps/expo/src/components/BallotMeasuresSection.tsx @@ -4,15 +4,7 @@ import { FontAwesome } from "@expo/vector-icons"; import type { Contest } from "@acme/api"; import { Text, View } from "~/components/Themed"; -import { - colors, - fontBody, - fontEditorial, - fontSize, - rd, - sp, - useTheme, -} from "~/styles"; +import { fontBody, fontEditorial, fontSize, rd, sp, useTheme } from "~/styles"; interface BallotMeasuresSectionProps { measures: Contest[]; @@ -50,7 +42,8 @@ export function BallotMeasuresSection({ {measure.referendumSubtitle} )} - {(measure.referendumProStatement ?? measure.referendumConStatement) && ( + {(measure.referendumProStatement ?? + measure.referendumConStatement) && ( {measure.referendumProStatement && ( @@ -78,29 +71,35 @@ export function BallotMeasuresSection({ ); } +const colors = { + white: "#FFFFFF", + civicBlue: "#4A7CFF", + textMuted: "#8A8FA0", +}; + const styles = StyleSheet.create({ container: { - marginHorizontal: sp.md, - marginBottom: sp.lg, + marginHorizontal: sp[4], + marginBottom: sp[6], }, sectionTitle: { fontFamily: fontEditorial.bold, fontSize: fontSize.lg, color: colors.white, - marginBottom: sp.sm, + marginBottom: sp[3], }, card: { - padding: sp.md, + padding: sp[4], borderRadius: rd.md, - marginBottom: sp.sm, + marginBottom: sp[3], }, badge: { backgroundColor: colors.civicBlue, alignSelf: "flex-start", paddingVertical: 2, - paddingHorizontal: sp.sm, - borderRadius: rd.xs, - marginBottom: sp.sm, + paddingHorizontal: sp[3], + borderRadius: 4, + marginBottom: sp[3], }, badgeText: { fontFamily: fontBody.semibold, @@ -112,16 +111,16 @@ const styles = StyleSheet.create({ fontFamily: fontBody.semibold, fontSize: fontSize.base, color: colors.white, - marginBottom: sp.xs, + marginBottom: sp[2], }, subtitle: { fontFamily: fontBody.regular, fontSize: fontSize.sm, color: colors.textMuted, - marginBottom: sp.sm, + marginBottom: sp[3], }, arguments: { - marginTop: sp.xs, + marginTop: sp[2], }, argument: { fontFamily: fontBody.regular, @@ -135,7 +134,7 @@ const styles = StyleSheet.create({ }, chevron: { position: "absolute", - right: sp.md, + right: sp[4], top: "50%", }, }); diff --git a/apps/expo/src/components/CandidatesSection.tsx b/apps/expo/src/components/CandidatesSection.tsx index f798993..9810c0f 100644 --- a/apps/expo/src/components/CandidatesSection.tsx +++ b/apps/expo/src/components/CandidatesSection.tsx @@ -5,15 +5,7 @@ import { FontAwesome } from "@expo/vector-icons"; import type { Contest } from "@acme/api"; import { Text, View } from "~/components/Themed"; -import { - colors, - fontBody, - fontEditorial, - fontSize, - rd, - sp, - useTheme, -} from "~/styles"; +import { fontBody, fontEditorial, fontSize, rd, sp, useTheme } from "~/styles"; interface CandidatesSectionProps { contests: Contest[]; @@ -82,46 +74,51 @@ export function CandidatesSection({ ); } +const colors = { + white: "#FFFFFF", + textMuted: "#8A8FA0", +}; + const styles = StyleSheet.create({ container: { - marginHorizontal: sp.md, - marginBottom: sp.lg, + marginHorizontal: sp[4], + marginBottom: sp[6], }, sectionTitle: { fontFamily: fontEditorial.bold, fontSize: fontSize.lg, color: colors.white, - marginBottom: sp.sm, + marginBottom: sp[3], }, raceGroup: { - marginBottom: sp.md, + marginBottom: sp[4], }, raceTitle: { fontFamily: fontBody.semibold, fontSize: fontSize.sm, color: colors.textMuted, - marginBottom: sp.sm, + marginBottom: sp[3], textTransform: "uppercase", letterSpacing: 0.5, }, candidateCard: { flexDirection: "row", alignItems: "center", - padding: sp.sm, + padding: sp[3], borderRadius: rd.md, - marginBottom: sp.xs, + marginBottom: sp[2], }, photo: { width: 44, height: 44, borderRadius: 22, - marginRight: sp.sm, + marginRight: sp[3], }, photoPlaceholder: { width: 44, height: 44, borderRadius: 22, - marginRight: sp.sm, + marginRight: sp[3], justifyContent: "center", alignItems: "center", }, diff --git a/apps/expo/src/components/ElectionBanner.tsx b/apps/expo/src/components/ElectionBanner.tsx index 0825af6..02fccf1 100644 --- a/apps/expo/src/components/ElectionBanner.tsx +++ b/apps/expo/src/components/ElectionBanner.tsx @@ -2,7 +2,7 @@ import { StyleSheet, TouchableOpacity } from "react-native"; import { FontAwesome } from "@expo/vector-icons"; import { Text, View } from "~/components/Themed"; -import { colors, fontBody, fontSize, rd, sp, useTheme } from "~/styles"; +import { fontBody, fontSize, rd, sp, useTheme } from "~/styles"; interface ElectionBannerProps { daysUntil: number; @@ -25,27 +25,39 @@ export function ElectionBanner({ - {daysUntil} days until {electionName} + {daysUntil} days until{" "} + {electionName} Know what's on your ballot - + See My Ballot - + ); } +const colors = { + white: "#FFFFFF", + black: "#000000", + civicBlue: "#4A7CFF", + textMuted: "#8A8FA0", +}; + const styles = StyleSheet.create({ container: { flexDirection: "row", - marginHorizontal: sp.md, - marginBottom: sp.md, + marginHorizontal: sp[4], + marginBottom: sp[4], borderRadius: rd.md, overflow: "hidden", }, @@ -58,17 +70,17 @@ const styles = StyleSheet.create({ flexDirection: "row", alignItems: "center", justifyContent: "space-between", - padding: sp.md, + padding: sp[4], }, textContainer: { flex: 1, - marginRight: sp.md, + marginRight: sp[4], }, headline: { fontFamily: fontBody.medium, fontSize: fontSize.sm, color: colors.white, - marginBottom: sp.xs, + marginBottom: sp[2], }, days: { fontFamily: fontBody.bold, @@ -83,10 +95,10 @@ const styles = StyleSheet.create({ flexDirection: "row", alignItems: "center", backgroundColor: colors.white, - paddingVertical: sp.sm, - paddingHorizontal: sp.md, + paddingVertical: sp[3], + paddingHorizontal: sp[4], borderRadius: 9999, - gap: sp.xs, + gap: sp[2], }, ctaText: { fontFamily: fontBody.semibold, @@ -95,8 +107,8 @@ const styles = StyleSheet.create({ }, dismiss: { position: "absolute", - top: sp.sm, - right: sp.sm, - padding: sp.xs, + top: sp[3], + right: sp[3], + padding: sp[2], }, }); diff --git a/apps/expo/src/components/KeyDatesSection.tsx b/apps/expo/src/components/KeyDatesSection.tsx index 449d0df..09bbdaf 100644 --- a/apps/expo/src/components/KeyDatesSection.tsx +++ b/apps/expo/src/components/KeyDatesSection.tsx @@ -1,15 +1,7 @@ import { ScrollView, StyleSheet } from "react-native"; import { Text, View } from "~/components/Themed"; -import { - colors, - fontBody, - fontEditorial, - fontSize, - rd, - sp, - useTheme, -} from "~/styles"; +import { fontBody, fontEditorial, fontSize, rd, sp, useTheme } from "~/styles"; import { daysUntil, formatDate } from "~/utils/dates"; interface KeyDate { @@ -34,11 +26,11 @@ export function KeyDatesSection({ electionDate }: KeyDatesSectionProps) { const dates: KeyDate[] = [ { label: "Registration Deadline", - date: registrationDeadline.toISOString().split("T")[0]!, + date: registrationDeadline.toISOString().split("T")[0] ?? "", }, { label: "Early Voting Starts", - date: earlyVotingStart.toISOString().split("T")[0]!, + date: earlyVotingStart.toISOString().split("T")[0] ?? "", }, { label: "Election Day", @@ -58,7 +50,8 @@ export function KeyDatesSection({ electionDate }: KeyDatesSectionProps) { const days = daysUntil(item.date); const isPassed = days < 0; const isNext = - !isPassed && dates.findIndex((d) => daysUntil(d.date) >= 0) === index; + !isPassed && + dates.findIndex((d) => daysUntil(d.date) >= 0) === index; return ( - {isPassed ? "Passed" : days === 0 ? "Today!" : `in ${days} days`} + {isPassed + ? "Passed" + : days === 0 + ? "Today!" + : `in ${days} days`} ); @@ -92,23 +89,29 @@ export function KeyDatesSection({ electionDate }: KeyDatesSectionProps) { ); } +const colors = { + white: "#FFFFFF", + civicBlue: "#4A7CFF", + textMuted: "#8A8FA0", +}; + const styles = StyleSheet.create({ container: { - marginBottom: sp.lg, + marginBottom: sp[6], }, sectionTitle: { fontFamily: fontEditorial.bold, fontSize: fontSize.lg, color: colors.white, - marginHorizontal: sp.md, - marginBottom: sp.sm, + marginHorizontal: sp[4], + marginBottom: sp[3], }, scrollContent: { - paddingHorizontal: sp.md, - gap: sp.sm, + paddingHorizontal: sp[4], + gap: sp[3], }, card: { - padding: sp.md, + padding: sp[4], borderRadius: rd.md, minWidth: 140, }, @@ -120,13 +123,13 @@ const styles = StyleSheet.create({ fontFamily: fontBody.medium, fontSize: fontSize.xs, color: colors.textMuted, - marginBottom: sp.xs, + marginBottom: sp[2], }, date: { fontFamily: fontBody.semibold, fontSize: fontSize.sm, color: colors.white, - marginBottom: sp.xs, + marginBottom: sp[2], }, countdown: { fontFamily: fontBody.regular, diff --git a/apps/expo/src/components/LocalBillsSection.tsx b/apps/expo/src/components/LocalBillsSection.tsx index 4e5cba3..5fbaa98 100644 --- a/apps/expo/src/components/LocalBillsSection.tsx +++ b/apps/expo/src/components/LocalBillsSection.tsx @@ -2,18 +2,11 @@ import { ActivityIndicator, StyleSheet, TouchableOpacity } from "react-native"; import { FontAwesome } from "@expo/vector-icons"; import { useQuery } from "@tanstack/react-query"; -import { legistar, type LegistarMatter } from "@acme/api"; +import type { LegistarMatter } from "@acme/api"; +import { legistar } from "@acme/api"; import { Text, View } from "~/components/Themed"; -import { - colors, - fontBody, - fontEditorial, - fontSize, - rd, - sp, - useTheme, -} from "~/styles"; +import { fontBody, fontEditorial, fontSize, rd, sp, useTheme } from "~/styles"; interface LocalBillsSectionProps { onBillPress?: (bill: LegistarMatter) => void; @@ -32,7 +25,10 @@ export function LocalBillsSection({ onBillPress }: LocalBillsSectionProps) { const allBills = [ ...sanjose.map((b) => ({ ...b, jurisdiction: "San Jose" })), - ...santaclara.map((b) => ({ ...b, jurisdiction: "Santa Clara County" })), + ...santaclara.map((b) => ({ + ...b, + jurisdiction: "Santa Clara County", + })), ]; return allBills @@ -65,7 +61,10 @@ export function LocalBillsSection({ onBillPress }: LocalBillsSectionProps) { - {(bill as LegistarMatter & { jurisdiction: string }).jurisdiction} + { + (bill as LegistarMatter & { jurisdiction: string }) + .jurisdiction + } {bill.MatterStatusName} @@ -74,7 +73,11 @@ export function LocalBillsSection({ onBillPress }: LocalBillsSectionProps) { {bill.MatterFile} - + ))} @@ -85,25 +88,31 @@ export function LocalBillsSection({ onBillPress }: LocalBillsSectionProps) { ); } +const colors = { + white: "#FFFFFF", + civicBlue: "#4A7CFF", + textMuted: "#8A8FA0", +}; + const styles = StyleSheet.create({ container: { - marginHorizontal: sp.md, - marginBottom: sp.lg, + marginHorizontal: sp[4], + marginBottom: sp[6], }, sectionTitle: { fontFamily: fontEditorial.bold, fontSize: fontSize.lg, color: colors.white, - marginBottom: sp.sm, + marginBottom: sp[3], }, loader: { - marginVertical: sp.lg, + marginVertical: sp[6], }, card: { flexDirection: "row", alignItems: "center", borderRadius: rd.md, - marginBottom: sp.sm, + marginBottom: sp[3], overflow: "hidden", }, cardAccent: { @@ -113,12 +122,12 @@ const styles = StyleSheet.create({ }, cardContent: { flex: 1, - padding: sp.md, + padding: sp[4], }, meta: { flexDirection: "row", - gap: sp.sm, - marginBottom: sp.xs, + gap: sp[3], + marginBottom: sp[2], }, jurisdiction: { fontFamily: fontBody.semibold, @@ -135,7 +144,7 @@ const styles = StyleSheet.create({ fontFamily: fontBody.medium, fontSize: fontSize.sm, color: colors.white, - marginBottom: sp.xs, + marginBottom: sp[2], }, file: { fontFamily: fontBody.regular, @@ -147,6 +156,6 @@ const styles = StyleSheet.create({ fontSize: fontSize.sm, color: colors.textMuted, textAlign: "center", - marginVertical: sp.lg, + marginVertical: sp[6], }, }); diff --git a/apps/expo/src/components/MyBallotSection.tsx b/apps/expo/src/components/MyBallotSection.tsx index 74e611a..eb2e653 100644 --- a/apps/expo/src/components/MyBallotSection.tsx +++ b/apps/expo/src/components/MyBallotSection.tsx @@ -6,19 +6,21 @@ import { TouchableOpacity, } from "react-native"; import { FontAwesome } from "@expo/vector-icons"; +import { useQuery } from "@tanstack/react-query"; + +import type { Contest } from "@acme/api"; import { Text, View } from "~/components/Themed"; -import { - colors, - fontBody, - fontEditorial, - fontSize, - rd, - sp, - useTheme, -} from "~/styles"; +import { fontBody, fontEditorial, fontSize, rd, sp, useTheme } from "~/styles"; import { trpc } from "~/utils/api"; +const colors = { + white: "#FFFFFF", + black: "#000000", + civicBlue: "#4A7CFF", + textMuted: "#8A8FA0", +}; + interface MyBallotSectionProps { address: string | null; onAddressSubmit: (address: string) => void; @@ -33,10 +35,10 @@ export function MyBallotSection({ const { theme } = useTheme(); const [inputValue, setInputValue] = useState(""); - const voterInfoQuery = trpc.civic.getVoterInfo.useQuery( - { address: address ?? "" }, - { enabled: !!address }, - ); + const voterInfoQuery = useQuery({ + ...trpc.civic.getVoterInfo.queryOptions({ address: address ?? "" }), + enabled: !!address, + }); if (!address) { return ( @@ -81,7 +83,7 @@ export function MyBallotSection({ )} - {voterInfoQuery.data?.contests?.map((contest, index) => ( + {voterInfoQuery.data?.contests?.map((contest: Contest, index: number) => ( { AsyncStorage.getItem(ADDRESS_KEY) - .then((value) => { + .then((value: string | null) => { setAddressState(value); setIsLoading(false); }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3db3a0c..5467977 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,6 +143,9 @@ importers: '@legendapp/list': specifier: ^2.0.19 version: 2.0.19(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) + '@react-native-async-storage/async-storage': + specifier: ^3.1.0 + version: 3.1.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) '@ronradtke/react-native-markdown-display': specifier: ^8.1.0 version: 8.1.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) @@ -3365,6 +3368,12 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-native-async-storage/async-storage@3.1.0': + resolution: {integrity: sha512-ENwbn3kj/dOapdR3GVgfX5x9f9/rxHnsqol1bUkt26lrv0aAq6UoXnTlv7y2dT7RH0AShh9B1+KAPDVjJXnk0w==} + peerDependencies: + react: '*' + react-native: '*' + '@react-native/assets-registry@0.79.6': resolution: {integrity: sha512-UVSP1224PWg0X+mRlZNftV5xQwZGfawhivuW8fGgxNK9MS/U84xZ+16lkqcPh1ank6MOt239lIWHQ1S33CHgqA==} engines: {node: '>=18'} @@ -5838,6 +5847,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + idb@8.0.3: + resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -9674,7 +9686,7 @@ snapshots: metro-babel-transformer: 0.83.1 metro-cache: 0.82.5 metro-cache-key: 0.83.1 - metro-config: 0.82.5(bufferutil@4.1.0) + metro-config: 0.82.5(bufferutil@4.1.0)(utf-8-validate@6.0.4) metro-core: 0.82.5 metro-file-map: 0.82.5 metro-resolver: 0.82.5 @@ -11085,6 +11097,12 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-native-async-storage/async-storage@3.1.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0)': + dependencies: + idb: 8.0.3 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4) + '@react-native/assets-registry@0.79.6': {} '@react-native/assets-registry@0.81.4': {} @@ -11233,7 +11251,7 @@ snapshots: debug: 2.6.9 invariant: 2.2.4 metro: 0.82.5(bufferutil@4.1.0)(utf-8-validate@6.0.4) - metro-config: 0.82.5(bufferutil@4.1.0) + metro-config: 0.82.5(bufferutil@4.1.0)(utf-8-validate@6.0.4) metro-core: 0.82.5 semver: 7.7.4 transitivePeerDependencies: @@ -13952,6 +13970,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb@8.0.3: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -14548,6 +14568,21 @@ snapshots: - supports-color - utf-8-validate + metro-config@0.82.5(bufferutil@4.1.0)(utf-8-validate@6.0.4): + dependencies: + connect: 3.7.0 + cosmiconfig: 5.2.1 + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.82.5(bufferutil@4.1.0)(utf-8-validate@6.0.4) + metro-cache: 0.82.5 + metro-core: 0.82.5 + metro-runtime: 0.82.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + metro-core@0.82.5: dependencies: flow-enums-runtime: 0.0.6 @@ -14732,7 +14767,7 @@ snapshots: metro-babel-transformer: 0.82.5 metro-cache: 0.82.5 metro-cache-key: 0.82.5 - metro-config: 0.82.5(bufferutil@4.1.0) + metro-config: 0.82.5(bufferutil@4.1.0)(utf-8-validate@6.0.4) metro-core: 0.82.5 metro-file-map: 0.82.5 metro-resolver: 0.82.5 From 0f74807406a793042ed38b168734167b2d937832 Mon Sep 17 00:00:00 2001 From: ThatXliner Date: Wed, 27 May 2026 09:17:56 -0700 Subject: [PATCH 12/13] :art: style(auth): apply lint formatting fixes --- packages/auth/src/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index e9e1f81..a7147f7 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -12,12 +12,14 @@ function expoPlugin(options?: { disableOriginOverride?: boolean }) { init: () => { return { options: { - trustedOrigins: process.env.NODE_ENV === "development" ? ["exp://"] : [], + trustedOrigins: + process.env.NODE_ENV === "development" ? ["exp://"] : [], }, }; }, async onRequest(request: Request) { - if (options?.disableOriginOverride || request.headers.get("origin")) return; + if (options?.disableOriginOverride || request.headers.get("origin")) + return; // Expo native clients send their origin separately, so mirror it for Better Auth's origin check. const expoOrigin = request.headers.get("expo-origin"); From 275a315419776e98315db3b1c3fab48ef1796532 Mon Sep 17 00:00:00 2001 From: ThatXliner Date: Wed, 27 May 2026 09:17:56 -0700 Subject: [PATCH 13/13] :sparkles: feat(api): add integrations barrel export --- packages/api/src/integrations/index.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/api/src/integrations/index.ts diff --git a/packages/api/src/integrations/index.ts b/packages/api/src/integrations/index.ts new file mode 100644 index 0000000..280d893 --- /dev/null +++ b/packages/api/src/integrations/index.ts @@ -0,0 +1 @@ +export * from "./legistar";