236 lines
9.0 KiB
TypeScript
236 lines
9.0 KiB
TypeScript
"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>
|
|
)
|
|
}
|
|
|