Initial commit
This commit is contained in:
86
frontend/lib/api.ts
Normal file
86
frontend/lib/api.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import axios from "axios";
|
||||
import type {
|
||||
Bill,
|
||||
BillAction,
|
||||
BillDetail,
|
||||
DashboardData,
|
||||
Follow,
|
||||
Member,
|
||||
NewsArticle,
|
||||
PaginatedResponse,
|
||||
SettingsData,
|
||||
TrendScore,
|
||||
} from "./types";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL || "",
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Bills
|
||||
export const billsAPI = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
apiClient.get<PaginatedResponse<Bill>>("/api/bills", { params }).then((r) => r.data),
|
||||
get: (id: string) =>
|
||||
apiClient.get<BillDetail>(`/api/bills/${id}`).then((r) => r.data),
|
||||
getActions: (id: string) =>
|
||||
apiClient.get<BillAction[]>(`/api/bills/${id}/actions`).then((r) => r.data),
|
||||
getNews: (id: string) =>
|
||||
apiClient.get<NewsArticle[]>(`/api/bills/${id}/news`).then((r) => r.data),
|
||||
getTrend: (id: string, days?: number) =>
|
||||
apiClient.get<TrendScore[]>(`/api/bills/${id}/trend`, { params: { days } }).then((r) => r.data),
|
||||
};
|
||||
|
||||
// Members
|
||||
export const membersAPI = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
apiClient.get<PaginatedResponse<Member>>("/api/members", { params }).then((r) => r.data),
|
||||
get: (id: string) =>
|
||||
apiClient.get<Member>(`/api/members/${id}`).then((r) => r.data),
|
||||
getBills: (id: string, params?: Record<string, unknown>) =>
|
||||
apiClient.get<PaginatedResponse<Bill>>(`/api/members/${id}/bills`, { params }).then((r) => r.data),
|
||||
};
|
||||
|
||||
// Follows
|
||||
export const followsAPI = {
|
||||
list: () =>
|
||||
apiClient.get<Follow[]>("/api/follows").then((r) => r.data),
|
||||
add: (follow_type: string, follow_value: string) =>
|
||||
apiClient.post<Follow>("/api/follows", { follow_type, follow_value }).then((r) => r.data),
|
||||
remove: (id: number) =>
|
||||
apiClient.delete(`/api/follows/${id}`),
|
||||
};
|
||||
|
||||
// Dashboard
|
||||
export const dashboardAPI = {
|
||||
get: () =>
|
||||
apiClient.get<DashboardData>("/api/dashboard").then((r) => r.data),
|
||||
};
|
||||
|
||||
// Search
|
||||
export const searchAPI = {
|
||||
search: (q: string) =>
|
||||
apiClient.get<{ bills: Bill[]; members: Member[] }>("/api/search", { params: { q } }).then((r) => r.data),
|
||||
};
|
||||
|
||||
// Settings
|
||||
export const settingsAPI = {
|
||||
get: () =>
|
||||
apiClient.get<SettingsData>("/api/settings").then((r) => r.data),
|
||||
update: (key: string, value: string) =>
|
||||
apiClient.put("/api/settings", { key, value }).then((r) => r.data),
|
||||
testLLM: () =>
|
||||
apiClient.post("/api/settings/test-llm").then((r) => r.data),
|
||||
};
|
||||
|
||||
// Admin
|
||||
export const adminAPI = {
|
||||
triggerPoll: () =>
|
||||
apiClient.post("/api/admin/trigger-poll").then((r) => r.data),
|
||||
triggerMemberSync: () =>
|
||||
apiClient.post("/api/admin/trigger-member-sync").then((r) => r.data),
|
||||
triggerTrendScores: () =>
|
||||
apiClient.post("/api/admin/trigger-trend-scores").then((r) => r.data),
|
||||
getTaskStatus: (taskId: string) =>
|
||||
apiClient.get(`/api/admin/task-status/${taskId}`).then((r) => r.data),
|
||||
};
|
||||
46
frontend/lib/hooks/useBills.ts
Normal file
46
frontend/lib/hooks/useBills.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { billsAPI } from "../api";
|
||||
|
||||
export function useBills(params?: Record<string, unknown>) {
|
||||
return useQuery({
|
||||
queryKey: ["bills", params],
|
||||
queryFn: () => billsAPI.list(params),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useBill(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ["bill", id],
|
||||
queryFn: () => billsAPI.get(id),
|
||||
staleTime: 2 * 60 * 1000,
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
export function useBillActions(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ["bill-actions", id],
|
||||
queryFn: () => billsAPI.getActions(id),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
export function useBillNews(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ["bill-news", id],
|
||||
queryFn: () => billsAPI.getNews(id),
|
||||
staleTime: 10 * 60 * 1000,
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
export function useBillTrend(id: string, days = 30) {
|
||||
return useQuery({
|
||||
queryKey: ["bill-trend", id, days],
|
||||
queryFn: () => billsAPI.getTrend(id, days),
|
||||
staleTime: 60 * 60 * 1000,
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
11
frontend/lib/hooks/useDashboard.ts
Normal file
11
frontend/lib/hooks/useDashboard.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { dashboardAPI } from "../api";
|
||||
|
||||
export function useDashboard() {
|
||||
return useQuery({
|
||||
queryKey: ["dashboard"],
|
||||
queryFn: () => dashboardAPI.get(),
|
||||
staleTime: 2 * 60 * 1000,
|
||||
refetchInterval: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
32
frontend/lib/hooks/useFollows.ts
Normal file
32
frontend/lib/hooks/useFollows.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { followsAPI } from "../api";
|
||||
|
||||
export function useFollows() {
|
||||
return useQuery({
|
||||
queryKey: ["follows"],
|
||||
queryFn: () => followsAPI.list(),
|
||||
staleTime: 30 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAddFollow() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ type, value }: { type: string; value: string }) =>
|
||||
followsAPI.add(type, value),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["follows"] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveFollow() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => followsAPI.remove(id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["follows"] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useIsFollowing(type: string, value: string) {
|
||||
const { data: follows = [] } = useFollows();
|
||||
return follows.find((f) => f.follow_type === type && f.follow_value === value);
|
||||
}
|
||||
28
frontend/lib/hooks/useMembers.ts
Normal file
28
frontend/lib/hooks/useMembers.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { membersAPI } from "../api";
|
||||
|
||||
export function useMembers(params?: Record<string, unknown>) {
|
||||
return useQuery({
|
||||
queryKey: ["members", params],
|
||||
queryFn: () => membersAPI.list(params),
|
||||
staleTime: 10 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useMember(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ["member", id],
|
||||
queryFn: () => membersAPI.get(id),
|
||||
staleTime: 10 * 60 * 1000,
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
export function useMemberBills(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ["member-bills", id],
|
||||
queryFn: () => membersAPI.getBills(id),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
103
frontend/lib/types.ts
Normal file
103
frontend/lib/types.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
export interface Member {
|
||||
bioguide_id: string;
|
||||
name: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
party?: string;
|
||||
state?: string;
|
||||
chamber?: string;
|
||||
district?: string;
|
||||
photo_url?: string;
|
||||
}
|
||||
|
||||
export interface BriefSchema {
|
||||
id: number;
|
||||
summary?: string;
|
||||
key_points?: string[];
|
||||
risks?: string[];
|
||||
deadlines?: { date: string | null; description: string }[];
|
||||
topic_tags?: string[];
|
||||
llm_provider?: string;
|
||||
llm_model?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface TrendScore {
|
||||
score_date: string;
|
||||
newsapi_count: number;
|
||||
gnews_count: number;
|
||||
gtrends_score: number;
|
||||
composite_score: number;
|
||||
}
|
||||
|
||||
export interface BillAction {
|
||||
id: number;
|
||||
action_date?: string;
|
||||
action_text?: string;
|
||||
action_type?: string;
|
||||
chamber?: string;
|
||||
}
|
||||
|
||||
export interface NewsArticle {
|
||||
id: number;
|
||||
source?: string;
|
||||
headline?: string;
|
||||
url?: string;
|
||||
published_at?: string;
|
||||
relevance_score?: number;
|
||||
}
|
||||
|
||||
export interface Bill {
|
||||
bill_id: string;
|
||||
congress_number: number;
|
||||
bill_type: string;
|
||||
bill_number: number;
|
||||
title?: string;
|
||||
short_title?: string;
|
||||
introduced_date?: string;
|
||||
latest_action_date?: string;
|
||||
latest_action_text?: string;
|
||||
status?: string;
|
||||
chamber?: string;
|
||||
congress_url?: string;
|
||||
sponsor?: Member;
|
||||
latest_brief?: BriefSchema;
|
||||
latest_trend?: TrendScore;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface BillDetail extends Bill {
|
||||
actions: BillAction[];
|
||||
news_articles: NewsArticle[];
|
||||
trend_scores: TrendScore[];
|
||||
briefs: BriefSchema[];
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export interface Follow {
|
||||
id: number;
|
||||
follow_type: "bill" | "member" | "topic";
|
||||
follow_value: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface DashboardData {
|
||||
feed: Bill[];
|
||||
trending: Bill[];
|
||||
follows: { bills: number; members: number; topics: number };
|
||||
}
|
||||
|
||||
export interface SettingsData {
|
||||
llm_provider: string;
|
||||
llm_model: string;
|
||||
congress_poll_interval_minutes: number;
|
||||
newsapi_enabled: boolean;
|
||||
pytrends_enabled: boolean;
|
||||
}
|
||||
52
frontend/lib/utils.ts
Normal file
52
frontend/lib/utils.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatDate(date?: string | null): string {
|
||||
if (!date) return "—";
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export function billLabel(billType: string, billNumber: number): string {
|
||||
const labels: Record<string, string> = {
|
||||
hr: "H.R.",
|
||||
s: "S.",
|
||||
hjres: "H.J.Res.",
|
||||
sjres: "S.J.Res.",
|
||||
hconres: "H.Con.Res.",
|
||||
sconres: "S.Con.Res.",
|
||||
hres: "H.Res.",
|
||||
sres: "S.Res.",
|
||||
};
|
||||
return `${labels[billType?.toLowerCase()] ?? billType?.toUpperCase()} ${billNumber}`;
|
||||
}
|
||||
|
||||
export function partyColor(party?: string): string {
|
||||
if (!party) return "text-muted-foreground";
|
||||
const p = party.toLowerCase();
|
||||
if (p.includes("democrat") || p === "d") return "text-blue-500";
|
||||
if (p.includes("republican") || p === "r") return "text-red-500";
|
||||
return "text-yellow-500";
|
||||
}
|
||||
|
||||
export function partyBadgeColor(party?: string): string {
|
||||
if (!party) return "bg-muted text-muted-foreground";
|
||||
const p = party.toLowerCase();
|
||||
if (p.includes("democrat") || p === "d") return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200";
|
||||
if (p.includes("republican") || p === "r") return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200";
|
||||
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200";
|
||||
}
|
||||
|
||||
export function trendColor(score?: number): string {
|
||||
if (!score) return "text-muted-foreground";
|
||||
if (score >= 70) return "text-red-500";
|
||||
if (score >= 40) return "text-yellow-500";
|
||||
return "text-green-500";
|
||||
}
|
||||
Reference in New Issue
Block a user