"use client"; import { useState } from "react"; import { useQueries } from "@tanstack/react-query"; import Link from "next/link"; import { ChevronDown, ChevronRight, ExternalLink, Heart, Search, X } from "lucide-react"; import { useFollows, useRemoveFollow } from "@/lib/hooks/useFollows"; import { billsAPI, membersAPI } from "@/lib/api"; import { FollowButton } from "@/components/shared/FollowButton"; import { billLabel, chamberBadgeColor, cn, formatDate, partyBadgeColor } from "@/lib/utils"; import type { Follow } from "@/lib/types"; // ── Bill row ───────────────────────────────────────────────────────────────── function BillRow({ follow, bill }: { follow: Follow; bill?: ReturnType extends Promise ? T : never }) { const label = bill ? billLabel(bill.bill_type, bill.bill_number) : follow.follow_value; return (
{label} {bill?.chamber && ( {bill.chamber} )}
{bill ? (bill.short_title || bill.title || label) : Loading…} {bill?.latest_action_text && (

{bill.latest_action_date && {formatDate(bill.latest_action_date)} — } {bill.latest_action_text}

)}
); } // ── Member row ──────────────────────────────────────────────────────────────── function MemberRow({ follow, member, onRemove }: { follow: Follow; member?: ReturnType extends Promise ? T : never; onRemove: () => void; }) { return (
{member?.photo_url ? ( {member.name} ) : (
{member ? member.name[0] : "?"}
)}
{member?.name ?? follow.follow_value} {member?.party && ( {member.party} )}
{(member?.chamber || member?.state || member?.district) && (

{[member.chamber, member.state, member.district ? `District ${member.district}` : null] .filter(Boolean).join(" · ")}

)} {member?.official_url && ( Official site )}
); } // ── Section accordion wrapper ───────────────────────────────────────────────── function Section({ title, count, children }: { title: string; count: number; children: React.ReactNode }) { const [open, setOpen] = useState(true); return (
{open && children}
); } // ── Page ────────────────────────────────────────────────────────────────────── export default function FollowingPage() { const { data: follows = [], isLoading } = useFollows(); const remove = useRemoveFollow(); const [billSearch, setBillSearch] = useState(""); const [billChamber, setBillChamber] = useState(""); const [memberSearch, setMemberSearch] = useState(""); const [memberParty, setMemberParty] = useState(""); const [topicSearch, setTopicSearch] = useState(""); const bills = follows.filter((f) => f.follow_type === "bill"); const members = follows.filter((f) => f.follow_type === "member"); const topics = follows.filter((f) => f.follow_type === "topic"); // Batch-fetch bill + member data at page level so filters have access to titles/names. // Uses the same query keys as BillRow/MemberRow — React Query deduplicates, no extra calls. const billQueries = useQueries({ queries: bills.map((f) => ({ queryKey: ["bill", f.follow_value], queryFn: () => billsAPI.get(f.follow_value), staleTime: 2 * 60 * 1000, })), }); const memberQueries = useQueries({ queries: members.map((f) => ({ queryKey: ["member", f.follow_value], queryFn: () => membersAPI.get(f.follow_value), staleTime: 10 * 60 * 1000, })), }); // Filter bills const filteredBills = bills.filter((f, i) => { const bill = billQueries[i]?.data; if (billChamber && bill?.chamber?.toLowerCase() !== billChamber.toLowerCase()) return false; if (billSearch) { const q = billSearch.toLowerCase(); const label = bill ? billLabel(bill.bill_type, bill.bill_number).toLowerCase() : ""; const title = (bill?.short_title || bill?.title || "").toLowerCase(); const id = f.follow_value.toLowerCase(); if (!label.includes(q) && !title.includes(q) && !id.includes(q)) return false; } return true; }); // Filter members const filteredMembers = members.filter((f, i) => { const member = memberQueries[i]?.data; if (memberParty && member?.party !== memberParty) return false; if (memberSearch) { const q = memberSearch.toLowerCase(); const name = (member?.name || f.follow_value).toLowerCase(); if (!name.includes(q)) return false; } return true; }); // Filter topics const filteredTopics = topics.filter((f) => !topicSearch || f.follow_value.toLowerCase().includes(topicSearch.toLowerCase()) ); // Unique parties and chambers from loaded data for filter dropdowns const loadedChambers = [...new Set(billQueries.map((q) => q.data?.chamber).filter(Boolean))] as string[]; const loadedParties = [...new Set(memberQueries.map((q) => q.data?.party).filter(Boolean))] as string[]; if (isLoading) return
Loading...
; return (

Following

Manage what you follow

{/* Bills */}
{/* Search + filter bar */} {bills.length > 0 && (
setBillSearch(e.target.value)} className="w-full pl-8 pr-3 py-1.5 text-sm bg-card border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-primary" />
{loadedChambers.length > 1 && ( )}
)} {!bills.length ? (

No bills followed yet.

) : !filteredBills.length ? (

No bills match your filters.

) : ( filteredBills.map((f, i) => { const originalIndex = bills.indexOf(f); return ; }) )}
{/* Members */}
{members.length > 0 && (
setMemberSearch(e.target.value)} className="w-full pl-8 pr-3 py-1.5 text-sm bg-card border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-primary" />
{loadedParties.length > 1 && ( )}
)} {!members.length ? (

No members followed yet.

) : !filteredMembers.length ? (

No members match your filters.

) : ( filteredMembers.map((f, i) => { const originalIndex = members.indexOf(f); return ( remove.mutate(f.id)} /> ); }) )}
{/* Topics */}
{topics.length > 0 && (
setTopicSearch(e.target.value)} className="w-full pl-8 pr-3 py-1.5 text-sm bg-card border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-primary" />
)} {!topics.length ? (

No topics followed yet.

) : !filteredTopics.length ? (

No topics match your search.

) : (
{filteredTopics.map((f) => (
{f.follow_value.replace(/-/g, " ")}
))}
)}
); }