499 lines
28 KiB
TypeScript
499 lines
28 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import Link from "next/link"
|
|
import { use } from "react"
|
|
import {
|
|
ArrowLeft,
|
|
CheckCircle,
|
|
Clock,
|
|
ExternalLink,
|
|
MessageCircle,
|
|
ThumbsUp,
|
|
Users,
|
|
FileText,
|
|
CalendarDays,
|
|
LineChart,
|
|
AlertCircle
|
|
} from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import {
|
|
Tabs,
|
|
TabsContent,
|
|
TabsList,
|
|
TabsTrigger
|
|
} from "@/components/ui/tabs"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Separator } from "@/components/ui/separator"
|
|
import { Progress } from "@/components/ui/progress"
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip"
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
|
|
// Mock data for a single contribution
|
|
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,
|
|
dateCreated: "2023-10-05",
|
|
dateValidated: "2023-11-15",
|
|
status: "Validated",
|
|
resolutionId: 12,
|
|
resolutionTitle: "Education and Community Health Improvements",
|
|
metrics: {
|
|
sentimentScore: 0.87,
|
|
participationRate: 0.62,
|
|
consensusSpeed: "Fast",
|
|
implementationComplexity: "Medium",
|
|
},
|
|
sourceInsight: {
|
|
id: 3,
|
|
title: "After-School Programs",
|
|
description: "Create free after-school programs for K-8 students in public schools"
|
|
},
|
|
relatedPerspectives: [
|
|
{
|
|
id: 101,
|
|
username: "parent87",
|
|
content: "As a working parent, having free after-school programs would be life-changing for me and my children.",
|
|
date: "2023-09-15",
|
|
votes: 43,
|
|
},
|
|
{
|
|
id: 102,
|
|
username: "educator22",
|
|
content: "After-school programs provide crucial educational support and keep children engaged in positive activities.",
|
|
date: "2023-09-18",
|
|
votes: 38,
|
|
},
|
|
{
|
|
id: 103,
|
|
username: "communityLeader",
|
|
content: "These programs would reduce juvenile crime rates in neighborhoods where working parents can't be home until evening.",
|
|
date: "2023-09-22",
|
|
votes: 29,
|
|
},
|
|
{
|
|
id: 104,
|
|
username: "budgetAnalyst",
|
|
content: "The ROI is excellent - the cost of these programs is lower than dealing with the social issues that arise without them.",
|
|
date: "2023-09-25",
|
|
votes: 36,
|
|
},
|
|
{
|
|
id: 105,
|
|
username: "socialWorker42",
|
|
content: "Many families in underserved areas would greatly benefit from structured after-school care and enrichment.",
|
|
date: "2023-09-28",
|
|
votes: 41,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
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,
|
|
dateCreated: "2023-11-10",
|
|
dateValidated: "2023-12-03",
|
|
status: "Validated",
|
|
resolutionId: 12,
|
|
resolutionTitle: "Education and Community Health Improvements",
|
|
metrics: {
|
|
sentimentScore: 0.82,
|
|
participationRate: 0.58,
|
|
consensusSpeed: "Medium",
|
|
implementationComplexity: "High",
|
|
},
|
|
sourceInsight: {
|
|
id: 4,
|
|
title: "Community Health Clinics",
|
|
description: "Establish walk-in clinics in underserved areas with sliding scale fees"
|
|
},
|
|
relatedPerspectives: [
|
|
// Perspectives data would go here
|
|
]
|
|
},
|
|
// Other contributions would go here
|
|
]
|
|
|
|
export default function ContributionPage({ params }: { params: Promise<{ id: string }> }) {
|
|
// Properly handle the params with React.use()
|
|
const { id } = use(params)
|
|
const contributionId = parseInt(id)
|
|
const [activeTab, setActiveTab] = useState("overview")
|
|
|
|
// Find the contribution from mock data
|
|
const contribution = mockContributions.find(c => c.id === contributionId)
|
|
|
|
if (!contribution) {
|
|
return (
|
|
<div className="container mx-auto px-4 py-16 text-center">
|
|
<AlertCircle className="mx-auto h-16 w-16 text-muted-foreground mb-4" />
|
|
<h1 className="text-2xl font-bold mb-2">Contribution Not Found</h1>
|
|
<p className="text-muted-foreground mb-8">The contribution you're looking for doesn't exist or has been removed.</p>
|
|
<Button asChild>
|
|
<Link href="/contributions">
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Return to Contributions
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Approval percentage
|
|
const approvalPercentage = Math.round((contribution.votes.yes / (contribution.votes.yes + contribution.votes.no)) * 100)
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4 mb-8">
|
|
<div>
|
|
<Button variant="outline" size="sm" className="mb-4" asChild>
|
|
<Link href="/contributions">
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Back to Contributions
|
|
</Link>
|
|
</Button>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Badge className="text-xs">{contribution.category}</Badge>
|
|
<Badge variant="outline" className="text-xs">ID: {contribution.id}</Badge>
|
|
</div>
|
|
<h1 className="text-3xl font-bold flex items-center gap-2">
|
|
<CheckCircle className="h-6 w-6 text-primary" />
|
|
{contribution.title}
|
|
</h1>
|
|
<p className="mt-2 text-muted-foreground">{contribution.description}</p>
|
|
</div>
|
|
<div className="flex flex-col gap-2 items-start md:items-end">
|
|
<Badge variant="secondary" className="mb-2">
|
|
{contribution.status}
|
|
</Badge>
|
|
<div className="flex items-center gap-1 text-sm">
|
|
<CalendarDays className="h-4 w-4 mr-1 text-muted-foreground" />
|
|
<span className="text-muted-foreground">Validated: </span>
|
|
<span>{new Date(contribution.dateValidated).toLocaleDateString()}</span>
|
|
</div>
|
|
<Button size="sm" asChild>
|
|
<Link href={`/resolutions/${contribution.resolutionId}`}>
|
|
<FileText className="mr-2 h-4 w-4" />
|
|
View in Resolution #{contribution.resolutionId}
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Tabs defaultValue="overview" value={activeTab} onValueChange={setActiveTab}>
|
|
<TabsList className="grid w-full grid-cols-3">
|
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
<TabsTrigger value="perspectives">Source Perspectives</TabsTrigger>
|
|
<TabsTrigger value="metrics">Metrics & Analysis</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* Overview Tab */}
|
|
<TabsContent value="overview" className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Consensus Details</CardTitle>
|
|
<CardDescription>How this contribution reached consensus</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-6">
|
|
<div>
|
|
<div className="flex justify-between mb-2 text-sm">
|
|
<span>Approval: {approvalPercentage}%</span>
|
|
<span>
|
|
<span className="text-green-600">{contribution.votes.yes} Yes</span> / <span className="text-red-600">{contribution.votes.no} No</span>
|
|
</span>
|
|
</div>
|
|
<Progress value={approvalPercentage} className="h-2" />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<p className="text-sm font-medium">Based On</p>
|
|
<p className="text-2xl font-bold">{contribution.perspectives}</p>
|
|
<p className="text-xs text-muted-foreground">unique perspectives</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium">Consensus Speed</p>
|
|
<p className="text-2xl font-bold">{contribution.metrics.consensusSpeed}</p>
|
|
<p className="text-xs text-muted-foreground">relative to average</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-2">Timeline</h4>
|
|
<div className="space-y-3">
|
|
<div className="flex gap-2">
|
|
<div className="flex flex-col items-center">
|
|
<div className="h-6 w-6 rounded-full bg-primary flex items-center justify-center text-primary-foreground">
|
|
<MessageCircle className="h-3 w-3" />
|
|
</div>
|
|
<div className="w-px h-full bg-muted-foreground/30 my-1"></div>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium">Perspectives Collection</p>
|
|
<p className="text-xs text-muted-foreground">Started: {new Date('2023-09-01').toLocaleDateString()}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<div className="flex flex-col items-center">
|
|
<div className="h-6 w-6 rounded-full bg-amber-500 flex items-center justify-center text-white">
|
|
<LineChart className="h-3 w-3" />
|
|
</div>
|
|
<div className="w-px h-full bg-muted-foreground/30 my-1"></div>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium">Insight Generated</p>
|
|
<p className="text-xs text-muted-foreground">Date: {new Date(contribution.dateCreated).toLocaleDateString()}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<div className="flex flex-col items-center">
|
|
<div className="h-6 w-6 rounded-full bg-green-500 flex items-center justify-center text-white">
|
|
<ThumbsUp className="h-3 w-3" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium">Consensus Reached</p>
|
|
<p className="text-xs text-muted-foreground">Date: {new Date(contribution.dateValidated).toLocaleDateString()}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Resolution Integration</CardTitle>
|
|
<CardDescription>How this contribution impacts policy</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-6">
|
|
<div>
|
|
<p className="text-sm font-medium mb-2">Included in Resolution</p>
|
|
<div className="flex items-center gap-2 p-3 border rounded-md">
|
|
<FileText className="h-5 w-5 text-blue-500" />
|
|
<div>
|
|
<p className="font-medium">Resolution #{contribution.resolutionId}</p>
|
|
<p className="text-sm text-muted-foreground">{contribution.resolutionTitle}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-2">Implementation Complexity</h4>
|
|
<Badge variant={contribution.metrics.implementationComplexity === "Low" ? "outline" :
|
|
contribution.metrics.implementationComplexity === "Medium" ? "secondary" :
|
|
"destructive"}>
|
|
{contribution.metrics.implementationComplexity}
|
|
</Badge>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
This contribution has been assessed as having {contribution.metrics.implementationComplexity.toLowerCase()}
|
|
implementation complexity based on required resources, timeline, and regulatory considerations.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-2">Source Insight</h4>
|
|
<Link href={`/insights/${contribution.sourceInsight.id}`} className="block p-3 border rounded-md hover:bg-accent transition-colors">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="font-medium">{contribution.sourceInsight.title}</p>
|
|
<p className="text-sm text-muted-foreground truncate">{contribution.sourceInsight.description}</p>
|
|
</div>
|
|
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* Perspectives Tab */}
|
|
<TabsContent value="perspectives" className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Source Perspectives</CardTitle>
|
|
<CardDescription>Original community input that led to this contribution</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-6">
|
|
{contribution.relatedPerspectives.map((perspective) => (
|
|
<div key={perspective.id} className="p-4 border rounded-lg">
|
|
<div className="flex justify-between items-start mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarImage src={`https://api.dicebear.com/7.x/initials/svg?seed=${perspective.username}`} alt={perspective.username} />
|
|
<AvatarFallback>{perspective.username.substring(0, 2).toUpperCase()}</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<p className="font-medium">{perspective.username}</p>
|
|
<p className="text-xs text-muted-foreground">{new Date(perspective.date).toLocaleDateString()}</p>
|
|
</div>
|
|
</div>
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="flex items-center gap-1">
|
|
<ThumbsUp className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm">{perspective.votes}</span>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Community upvotes</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
<p className="text-sm">{perspective.content}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
{/* Metrics Tab */}
|
|
<TabsContent value="metrics" className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Metrics & Analysis</CardTitle>
|
|
<CardDescription>Statistical breakdown of community engagement</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-2">Sentiment Analysis</h4>
|
|
<div className="flex items-center gap-4">
|
|
<div className="h-16 w-16 rounded-full border-4 border-green-500 flex items-center justify-center">
|
|
<span className="text-xl font-bold">{contribution.metrics.sentimentScore * 10}</span>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">Positive Sentiment</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{contribution.metrics.sentimentScore > 0.8 ? "Very Positive" :
|
|
contribution.metrics.sentimentScore > 0.6 ? "Positive" :
|
|
contribution.metrics.sentimentScore > 0.4 ? "Neutral" : "Mixed"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
AI analysis of perspective text shows strong support for this contribution.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-2">Demographic Distribution</h4>
|
|
<div className="space-y-2">
|
|
<div>
|
|
<div className="flex justify-between mb-1 text-xs">
|
|
<span>Age Groups</span>
|
|
</div>
|
|
<div className="flex h-2">
|
|
<div className="h-full bg-blue-300" style={{ width: "15%" }}></div>
|
|
<div className="h-full bg-blue-400" style={{ width: "25%" }}></div>
|
|
<div className="h-full bg-blue-500" style={{ width: "35%" }}></div>
|
|
<div className="h-full bg-blue-600" style={{ width: "20%" }}></div>
|
|
<div className="h-full bg-blue-700" style={{ width: "5%" }}></div>
|
|
</div>
|
|
<div className="flex justify-between text-xs mt-1">
|
|
<span>18-24</span>
|
|
<span>25-34</span>
|
|
<span>35-44</span>
|
|
<span>45-64</span>
|
|
<span>65+</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-2">Participation Rate</h4>
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative h-16 w-16">
|
|
<svg className="h-full w-full" viewBox="0 0 36 36">
|
|
<path
|
|
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
|
fill="none"
|
|
stroke="#eee"
|
|
strokeWidth="3"
|
|
/>
|
|
<path
|
|
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
|
fill="none"
|
|
stroke="#3b82f6"
|
|
strokeWidth="3"
|
|
strokeDasharray={`${contribution.metrics.participationRate * 100}, 100`}
|
|
/>
|
|
</svg>
|
|
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-sm font-semibold">
|
|
{Math.round(contribution.metrics.participationRate * 100)}%
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">Community Engagement</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{contribution.metrics.participationRate > 0.7 ? "Exceptional" :
|
|
contribution.metrics.participationRate > 0.5 ? "High" :
|
|
contribution.metrics.participationRate > 0.3 ? "Moderate" : "Low"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium mb-2">Geographic Impact</h4>
|
|
<div className="p-3 border rounded-md">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Users className="h-5 w-5 text-primary" />
|
|
<p className="font-medium">City-wide Relevance</p>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground mb-2">
|
|
This contribution has been flagged as having city-wide relevance with particular
|
|
significance for underserved communities.
|
|
</p>
|
|
<div className="flex gap-2">
|
|
<Badge variant="outline">Downtown</Badge>
|
|
<Badge variant="outline">South Side</Badge>
|
|
<Badge variant="outline">West End</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
<div className="mt-12 flex justify-center">
|
|
<Button variant="outline" asChild>
|
|
<Link href="/contributions">Return to All Contributions</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|