diff --git a/src/client/features/legislation/components/legislation-detail.tsx b/src/client/features/legislation/components/legislation-detail.tsx new file mode 100644 index 0000000..1bb9841 --- /dev/null +++ b/src/client/features/legislation/components/legislation-detail.tsx @@ -0,0 +1,89 @@ +import { useState } from "react" +import { useLegislation } from "../hooks/use-legislation" +import { useUserVote } from "../hooks/use-user-vote" +import { VoteWidget } from "./vote-widget" + +interface LegislationDetailProps { + legislationId: number +} + +export function LegislationDetail({ legislationId }: LegislationDetailProps) { + const { legislation, loading, error } = useLegislation(legislationId) + const { vote, castVote } = useUserVote(legislationId) + const [showFullText, setShowFullText] = useState(false) + + if (loading) { + return
Laden…
+ } + + if (error || !legislation) { + return ( +
+ {error ?? "Gesetzesvorlage nicht gefunden"} +
+ ) + } + + return ( +
+
+

+ {legislation.title} +

+ {legislation.beratungsstand && ( + + {legislation.beratungsstand} + + )} +
+ + {/* Summary or abstract */} +
+ {legislation.summary ? ( +

{legislation.summary}

+ ) : legislation.abstract ? ( +

+ {legislation.abstract} +

+ ) : ( +

+ Keine Zusammenfassung verfügbar +

+ )} +
+ + {/* Full text toggle */} + {legislation.fullText && ( +
+ + {showFullText && ( +
+							{legislation.fullText}
+						
+ )} +
+ )} + + {/* Vote section */} +
+

Dein Vote

+ + {vote && ( +

+ Du hast mit{" "} + + {vote === "ja" ? "Ja" : vote === "nein" ? "Nein" : "Enthaltung"} + {" "} + gestimmt. Du kannst deine Stimme jederzeit ändern. +

+ )} +
+
+ ) +} diff --git a/src/client/features/legislation/components/vote-widget.tsx b/src/client/features/legislation/components/vote-widget.tsx new file mode 100644 index 0000000..2ff082a --- /dev/null +++ b/src/client/features/legislation/components/vote-widget.tsx @@ -0,0 +1,42 @@ +import type { UserVoteChoice } from "../../../../shared/legislation-types" + +interface VoteWidgetProps { + currentVote: UserVoteChoice | null + onVote: (choice: UserVoteChoice) => void + disabled?: boolean +} + +const choices: { value: UserVoteChoice; label: string; color: string }[] = [ + { value: "ja", label: "Ja", color: "bg-green-600 hover:bg-green-700" }, + { value: "nein", label: "Nein", color: "bg-red-600 hover:bg-red-700" }, + { + value: "enthaltung", + label: "Enthaltung", + color: "bg-gray-500 hover:bg-gray-600", + }, +] + +export function VoteWidget({ currentVote, onVote, disabled }: VoteWidgetProps) { + return ( +
+ {choices.map((c) => { + const isSelected = currentVote === c.value + return ( + + ) + })} +
+ ) +} diff --git a/src/client/features/legislation/hooks/use-legislation.ts b/src/client/features/legislation/hooks/use-legislation.ts new file mode 100644 index 0000000..4f0b321 --- /dev/null +++ b/src/client/features/legislation/hooks/use-legislation.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from "react" +import type { LegislationDetail } from "../../../../shared/legislation-types" +import { fetchLegislation } from "../lib/legislation-api" + +interface UseLegislationReturn { + legislation: LegislationDetail | null + loading: boolean + error: string | null +} + +export function useLegislation(legislationId: number): UseLegislationReturn { + const [legislation, setLegislation] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + + async function load() { + try { + const data = await fetchLegislation(legislationId) + if (!cancelled) setLegislation(data) + } catch (err) { + if (!cancelled) setError(String(err)) + } finally { + if (!cancelled) setLoading(false) + } + } + + load() + return () => { + cancelled = true + } + }, [legislationId]) + + return { legislation, loading, error } +} diff --git a/src/client/features/legislation/hooks/use-user-vote.ts b/src/client/features/legislation/hooks/use-user-vote.ts new file mode 100644 index 0000000..377afc9 --- /dev/null +++ b/src/client/features/legislation/hooks/use-user-vote.ts @@ -0,0 +1,66 @@ +import { useDb } from "@/shared/db/provider" +import { useDeviceId } from "@/shared/hooks/use-device-id" +import { useCallback, useEffect, useState } from "react" +import type { UserVoteChoice } from "../../../../shared/legislation-types" +import { castVote as apiCastVote, fetchUserVote } from "../lib/legislation-api" +import { getUserVote, saveUserVote } from "../lib/user-votes-db" + +interface UseUserVoteReturn { + vote: UserVoteChoice | null + loading: boolean + castVote: (choice: UserVoteChoice) => Promise +} + +export function useUserVote(legislationId: number): UseUserVoteReturn { + const db = useDb() + const deviceId = useDeviceId() + const [vote, setVote] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + let cancelled = false + + async function load() { + // load from local DB first for instant UI + const local = await getUserVote(db, legislationId) + if (!cancelled && local) { + setVote(local.vote as UserVoteChoice) + setLoading(false) + } + + // sync from backend if device ID is available + if (deviceId) { + const remote = await fetchUserVote(legislationId, deviceId).catch( + () => null, + ) + if (!cancelled && remote) { + setVote(remote.vote) + await saveUserVote(db, legislationId, remote.vote) + } + } + + if (!cancelled) setLoading(false) + } + + load() + return () => { + cancelled = true + } + }, [db, legislationId, deviceId]) + + const cast = useCallback( + async (choice: UserVoteChoice) => { + // optimistic local update + setVote(choice) + await saveUserVote(db, legislationId, choice) + + // sync to backend + if (deviceId) { + await apiCastVote(legislationId, deviceId, choice) + } + }, + [db, legislationId, deviceId], + ) + + return { vote, loading, castVote: cast } +} diff --git a/src/client/features/legislation/index.ts b/src/client/features/legislation/index.ts new file mode 100644 index 0000000..114dacb --- /dev/null +++ b/src/client/features/legislation/index.ts @@ -0,0 +1 @@ +export { LegislationDetail } from "./components/legislation-detail" diff --git a/src/client/routes/app/legislation.$legislationId.tsx b/src/client/routes/app/legislation.$legislationId.tsx new file mode 100644 index 0000000..72ba4e1 --- /dev/null +++ b/src/client/routes/app/legislation.$legislationId.tsx @@ -0,0 +1,11 @@ +import { LegislationDetail } from "@/features/legislation" +import { createFileRoute } from "@tanstack/react-router" + +function LegislationPage() { + const { legislationId } = Route.useParams() + return +} + +export const Route = createFileRoute("/app/legislation/$legislationId")({ + component: LegislationPage, +})