236 lines
9.0 KiB
TypeScript
Raw Normal View History

2025-03-25 03:52:30 -04:00
"use client"
import { useState } from "react"
import Link from "next/link"
import { ArrowRight, ArrowUpDown, CheckCircle, ExternalLink, Filter, Search, SortAsc, SortDesc } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
// Mock data for contributions
const mockContributions = [
{
id: 1,
title: "After-School Programs",
description: "Create free after-school programs for K-8 students in public schools",
category: "Education",
votes: { yes: 312, no: 28 },
perspectives: 42,
date: "2023-11-15",
},
{
id: 2,
title: "Community Health Clinics",
description: "Establish walk-in clinics in underserved areas with sliding scale fees",
category: "Healthcare",
votes: { yes: 278, no: 42 },
perspectives: 36,
date: "2023-12-03",
},
{
id: 3,
title: "Public Library Expansion",
description: "Increase funding for public libraries to expand hours and digital resources",
category: "Education",
votes: { yes: 245, no: 35 },
perspectives: 29,
date: "2024-01-10",
},
{
id: 4,
title: "Green Space Preservation",
description: "Protect existing green spaces from development and create new community gardens",
category: "Environmental Policy",
votes: { yes: 289, no: 41 },
perspectives: 38,
date: "2024-01-22",
},
]
// Add state for search, filter, and sort
export default function ContributionsPage() {
const [searchQuery, setSearchQuery] = useState("")
const [categoryFilter, setCategoryFilter] = useState("all")
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc")
const [sortBy, setSortBy] = useState<"votes" | "date">("date")
// Filter and sort the contributions
const filteredContributions = mockContributions.filter((contribution) => {
// Filter by search query
if (
searchQuery &&
!contribution.title.toLowerCase().includes(searchQuery.toLowerCase()) &&
!contribution.description.toLowerCase().includes(searchQuery.toLowerCase())
)
return false
// Filter by category
if (categoryFilter !== "all" && contribution.category !== categoryFilter) return false
return true
})
// Sort the filtered contributions
const sortedContributions = [...filteredContributions].sort((a, b) => {
if (sortBy === "votes") {
const totalVotesA = a.votes.yes + a.votes.no
const totalVotesB = b.votes.yes + b.votes.no
return sortOrder === "desc" ? totalVotesB - totalVotesA : totalVotesA - totalVotesB
} else {
const dateA = new Date(a.date).getTime()
const dateB = new Date(b.date).getTime()
return sortOrder === "desc" ? dateB - dateA : dateA - dateB
}
})
// Add the search, filter, and sort UI
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold">Validated Contributions</h1>
<p className="mt-2 text-muted-foreground">
These insights have reached community consensus and become formal contributions.
</p>
</div>
{/* Filters and Search */}
<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="relative w-full md:w-72">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search contributions..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex flex-col gap-4 sm:flex-row">
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger className="w-full sm:w-[180px]">
<Filter className="mr-2 h-4 w-4" />
<SelectValue placeholder="Filter by category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
<SelectItem value="Environmental Policy">Environmental</SelectItem>
<SelectItem value="Education">Education</SelectItem>
<SelectItem value="Healthcare">Healthcare</SelectItem>
</SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="flex items-center gap-2">
<ArrowUpDown className="h-4 w-4" />
Sort by {sortBy === "votes" ? "Votes" : "Date"}
{sortOrder === "desc" ? <SortDesc className="ml-1 h-4 w-4" /> : <SortAsc className="ml-1 h-4 w-4" />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setSortBy("votes")}>Votes {sortBy === "votes" && "✓"}</DropdownMenuItem>
<DropdownMenuItem onClick={() => setSortBy("date")}>Date {sortBy === "date" && "✓"}</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setSortOrder(sortOrder === "desc" ? "asc" : "desc")}>
{sortOrder === "desc" ? "Ascending" : "Descending"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{sortedContributions.map((contribution) => (
<Card key={contribution.id}>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-primary" />
{contribution.title}
</CardTitle>
<CardDescription className="mt-1">{contribution.category}</CardDescription>
</div>
<Badge>Validated</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{contribution.description}</p>
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div>
<p className="font-medium">Consensus</p>
<p className="text-muted-foreground">
{Math.round((contribution.votes.yes / (contribution.votes.yes + contribution.votes.no)) * 100)}%
approval
</p>
</div>
<div>
<p className="font-medium">Based on</p>
<p className="text-muted-foreground">{contribution.perspectives} perspectives</p>
</div>
<div>
<p className="font-medium">Date Validated</p>
<p className="text-muted-foreground">{new Date(contribution.date).toLocaleDateString()}</p>
</div>
<div>
<p className="font-medium">Status</p>
<p className="text-muted-foreground">Added to Resolution #12</p>
</div>
</div>
</CardContent>
<CardFooter className="flex flex-col gap-4 sm:flex-row">
<Button variant="outline" className="w-full sm:w-auto" asChild>
<Link href={`/contributions/${contribution.id}`}>
View Details
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
<Button className="w-full sm:w-auto" asChild>
<Link href={`/resolutions`}>
View in Resolution
<ExternalLink className="ml-2 h-4 w-4" />
</Link>
</Button>
</CardFooter>
</Card>
))}
</div>
{sortedContributions.length === 0 && (
<div className="mt-12 text-center">
<p className="text-lg font-medium">No contributions match your filters</p>
<p className="mt-2 text-muted-foreground">Try adjusting your search or filters</p>
<Button
variant="outline"
className="mt-4"
onClick={() => {
setSearchQuery("")
setCategoryFilter("all")
}}
>
Clear All Filters
</Button>
</div>
)}
<div className="mt-12 flex justify-center">
<Button variant="outline" asChild>
<Link href="/insights">Return to Insights Dashboard</Link>
</Button>
</div>
</div>
)
}