ui prototyping
This commit is contained in:
parent
227740a13f
commit
66f170a21d
@ -57,7 +57,7 @@
|
||||
|
||||
## System Architecture Overview
|
||||
|
||||
- **Frontend**: A user-friendly interface (e.g., built with **Elm**) for submitting feedback, viewing legislation, and editing collaboratively.
|
||||
- **Frontend**: A user-friendly interface (e.g., built with **Next.js**) for submitting feedback, viewing legislation, and editing collaboratively.
|
||||
- **Backend**: Core logic (e.g., in **Go**) to process feedback, integrate AI, and interact with the blockchain.
|
||||
- **Blockchain**: **Hyperledger Fabric** for storing data and managing smart contracts.
|
||||
- **AI Services**: **Python**-based NLP and text generation models.
|
||||
|
@ -41,7 +41,7 @@ The system’s architecture is modular, allowing each component to be developed
|
||||
### 5. Frontend Layer
|
||||
- **Purpose**: Offers a user-friendly interface for feedback submission, editing, and tracking.
|
||||
- **Technology**:
|
||||
- Elm for a secure, functional UI.
|
||||
- Next.js for a secure, functional UI.
|
||||
- JavaScript for cryptographic tasks (e.g., ZKP generation).
|
||||
- **Key Features**:
|
||||
- Accessible design (WCAG-compliant).
|
||||
@ -68,7 +68,7 @@ We’ll use a phased, iterative approach to build the system, ensuring each comp
|
||||
1. Deploy Hyperledger Fabric network.
|
||||
2. Implement chaincode for feedback submission with simulated ZKPs (e.g., token-based).
|
||||
3. Build an AI microservice for feedback categorization and text generation.
|
||||
4. Create a basic Elm frontend for feedback submission.
|
||||
4. Create a basic Next.js frontend for feedback submission.
|
||||
- **Success Metrics**:
|
||||
- Blockchain stores test feedback.
|
||||
- AI accurately categorizes and generates text from sample data.
|
||||
|
@ -51,9 +51,9 @@ Your system can be broken down into six core services, each with distinct respon
|
||||
- Provide a user-friendly interface for feedback submission, editing, and tracking.
|
||||
- Handle client-side cryptographic operations (e.g., ZKP generation).
|
||||
- **Languages**:
|
||||
- **Elm**: For the core UI logic (secure, functional, and maintainable).
|
||||
- **Next.js**: For the core UI logic (secure, functional, and maintainable).
|
||||
- **JavaScript**: For cryptographic tasks (e.g., ZKP generation via `snarkjs`).
|
||||
- **Why Elm and JavaScript?**: Elm ensures a robust, type-safe UI, while JavaScript handles browser-based cryptography.
|
||||
- **Why Next.js and JavaScript?**: Next.js ensures a robust, type-safe UI, while JavaScript handles browser-based cryptography.
|
||||
- **Integration**:
|
||||
- Communicates with backend services via gRPC-Web or a proxy.
|
||||
- Uses WebSockets or polling for real-time updates (e.g., live legislative tracking).
|
||||
@ -81,7 +81,7 @@ legislative-platform/
|
||||
├── ai-service/ # Python: AI analysis and text generation
|
||||
├── zkp-service/ # Go: ZKP verification
|
||||
├── collaboration-service/ # Go: Git-like collaboration logic
|
||||
├── frontend-service/ # Elm + JS: User interface and client-side crypto
|
||||
├── frontend-service/ # Next.js + JS: User interface and client-side crypto
|
||||
├── integration-service/ # Go: External system integration
|
||||
├── shared/ # Common libraries, protos, etc.
|
||||
├── docs/ # Documentation
|
||||
|
@ -8,13 +8,13 @@
|
||||
- **Why**: While you’re more comfortable in Go, Python offers mature libraries for advanced cryptography, like blind signatures (e.g., `blind-signatures` package), which are less developed in Go. Python’s ecosystem also supports prototyping complex cryptographic components quickly.
|
||||
- **Use Case**: Implement cryptographic primitives like blind signatures in Python, then expose them to your Go services via gRPC.
|
||||
|
||||
### 3. **JavaScript and Elm for the Frontend**
|
||||
- **Why**: Your experience with JavaScript, combined with your colleague’s Elm expertise, makes this a strong frontend combo. Elm’s type safety and functional nature ensure a secure, reliable user interface, while JavaScript can handle client-side cryptographic tasks via interop.
|
||||
- **Use Case**: Use Elm to build the UI, guiding users through processes like submitting data or generating tokens. Leverage JavaScript libraries (e.g., `crypto-js`) for client-side cryptography when needed.
|
||||
### 3. **JavaScript and Next.js for the Frontend**
|
||||
- **Why**: Your experience with JavaScript, combined with your colleague’s Next.js expertise, makes this a strong frontend combo. Typescript’s type safety and functional nature ensure a secure, reliable user interface, while JavaScript can handle client-side cryptographic tasks via interop.
|
||||
- **Use Case**: Use Next.js to build the UI, guiding users through processes like submitting data or generating tokens. Leverage JavaScript libraries (e.g., `crypto-js`) for client-side cryptography when needed.
|
||||
|
||||
### 4. **Protocol Buffers and gRPC**
|
||||
- **Why**: Your preference for Protocol Buffers and gRPC aligns perfectly with building secure, efficient communication between services. Go has excellent gRPC support, and Connect-wrapped gRPC adds HTTP/1.1 compatibility for broader client access.
|
||||
- **Use Case**: Define service interfaces with Protocol Buffers and use gRPC for communication between your Go backend, Python cryptographic services, and Elm frontend (via JavaScript interop).
|
||||
- **Use Case**: Define service interfaces with Protocol Buffers and use gRPC for communication between your Go backend, Python cryptographic services, and Next.js frontend (via JavaScript interop).
|
||||
|
||||
---
|
||||
|
||||
@ -45,10 +45,10 @@
|
||||
- **What**: Transition to a blockchain like Ethereum for decentralization.
|
||||
- **How**: Write smart contracts in Solidity and use `go-ethereum` in your Go services to interact with the chain.
|
||||
|
||||
### **4. Frontend with Elm**
|
||||
- **What**: Build a secure, user-friendly interface in Elm, leveraging your colleague’s expertise.
|
||||
### **4. Frontend with Next.js**
|
||||
- **What**: Build a secure, user-friendly interface in Next.js, leveraging your colleague’s expertise.
|
||||
- **How**:
|
||||
- Handle user interactions (e.g., submitting opinions, generating tokens) in Elm.
|
||||
- Handle user interactions (e.g., submitting opinions, generating tokens) in Next.js.
|
||||
- For cryptographic operations (e.g., commitments), use JavaScript interop to call libraries like `crypto-js` or `sjcl`.
|
||||
|
||||
### **5. Communication with gRPC**
|
||||
@ -56,13 +56,13 @@
|
||||
- **How**:
|
||||
- Define your APIs in Protocol Buffers (e.g., `.proto` files).
|
||||
- Implement gRPC servers in Go for the backend and in Python for cryptographic services.
|
||||
- Use gRPC clients in Elm (via JavaScript interop) to connect to the backend.
|
||||
- Use gRPC clients in Next.js (via JavaScript interop) to connect to the backend.
|
||||
|
||||
---
|
||||
|
||||
## Why This Approach Works for You
|
||||
- **Plays to Your Strengths**: Go is your core skill, so it handles the heavy lifting. Python and JavaScript fill gaps where Go’s ecosystem is less mature, using languages you’ve worked with before.
|
||||
- **Leverages Your Team**: Your Elm developer can own the frontend, ensuring a high-quality UI while you focus on the backend and integration.
|
||||
- **Leverages Your Team**: Your Next.js developer can own the frontend, ensuring a high-quality UI while you focus on the backend and integration.
|
||||
- **Matches Your Preferences**: Protocol Buffers and gRPC are central to the architecture, providing the efficient, secure communication you enjoy working with.
|
||||
- **Scalable Design**: Start simple with a centralized database, then scale to a blockchain if needed, all while keeping your codebase modular and maintainable.
|
||||
|
||||
@ -71,11 +71,11 @@
|
||||
## Next Steps to Get Started
|
||||
1. **Go Backend**: Set up a basic gRPC server in Go using Protocol Buffers. Implement a simple hashing function with `crypto/sha256` to test commitments.
|
||||
2. **Python Cryptography**: Prototype blind signatures in Python with `blind-signatures` and expose them via a gRPC service.
|
||||
3. **Elm Frontend**: Work with your Elm developer to create a basic UI that sends requests to your Go backend via gRPC (using JS interop).
|
||||
3. **Next.js Frontend**: Work with your Next.js developer to create a basic UI that sends requests to your Go backend via gRPC (using JS interop).
|
||||
4. **ZKP Exploration**: Experiment with Circom and `snarkjs` to build a small ZKP circuit, then verify it in Go with `gnark`.
|
||||
5. **Ledger Setup**: Start with PostgreSQL in Go, storing hashed data, and plan for a blockchain pivot later if required.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
This approach lets you use Go for the core, Python for cryptography where Go lacks, Elm for a secure frontend, and gRPC for communication—all aligned with your skills and preferences. You’ll build a solid system that’s secure, efficient, and extensible, with room to grow into more advanced features like blockchain integration.
|
||||
This approach lets you use Go for the core, Python for cryptography where Go lacks, Next.js for a secure frontend, and gRPC for communication—all aligned with your skills and preferences. You’ll build a solid system that’s secure, efficient, and extensible, with room to grow into more advanced features like blockchain integration.
|
@ -3,7 +3,7 @@
|
||||
Your commitment to full decentralization is a brilliant shift—it aligns with blockchain’s ethos and makes your system resilient, uncensorable, and community-driven. Stepping outside your comfort zone is a worthwhile trade-off for this level of robustness. Here’s how it changes your system design, languages, architecture, and development approach:
|
||||
|
||||
#### 1. **Decentralized Frontend Hosting**
|
||||
- **Change**: Host your frontend (e.g., Elm app) on **IPFS** or **Arweave** instead of a traditional server.
|
||||
- **Change**: Host your frontend (e.g., Next.js app) on **IPFS** or **Arweave** instead of a traditional server.
|
||||
- **Impact**: Users access the app via decentralized gateways (e.g., `ipfs.io`), eliminating reliance on centralized hosting that could be shut down.
|
||||
- **Tech**: Use tools like **Fleek** or **Pinata** to deploy and pin your frontend on IPFS.
|
||||
- **Comfort Zone Shift**: You’ll need to learn IPFS deployment, but these tools provide straightforward workflows.
|
||||
@ -57,7 +57,7 @@ Your commitment to full decentralization is a brilliant shift—it aligns with b
|
||||
Here’s how decentralization reshapes your tech stack:
|
||||
|
||||
- **Frontend**:
|
||||
- **Language**: Elm remains, but you’ll add wallet integration (e.g., MetaMask) and IPFS gateway logic.
|
||||
- **Language**: Next.js remains, but you’ll add wallet integration (e.g., MetaMask) and IPFS gateway logic.
|
||||
- **Architecture**: Hosted on IPFS, interacting directly with smart contracts.
|
||||
- **Backend**:
|
||||
- **Language**: Go or TypeScript, but minimized—most logic shifts to smart contracts or decentralized services.
|
||||
@ -149,13 +149,13 @@ Even in a decentralized system, certain backend-like responsibilities persist, b
|
||||
|
||||
To clarify, here’s a quick comparison:
|
||||
|
||||
| **Responsibility** | **Traditional Backend** | **Decentralized Backend** |
|
||||
|---------------------------|------------------------------|-----------------------------------|
|
||||
| **Data Storage** | Central database | Blockchain (key data), IPFS (bulk data) |
|
||||
| **Business Logic** | Server-side code | Smart contracts |
|
||||
| **Heavy Computation** | Server processing | Decentralized compute platforms |
|
||||
| **Authentication** | Centralized login (e.g., OAuth) | Decentralized identity (e.g., SSI) |
|
||||
| **Communication** | WebSockets on servers | P2P protocols (e.g., libp2p) |
|
||||
| **Responsibility** | **Traditional Backend** | **Decentralized Backend** |
|
||||
|---------------------------|---------------------------------|-----------------------------------------|
|
||||
| **Data Storage** | Central database | Blockchain (key data), IPFS (bulk data) |
|
||||
| **Business Logic** | Server-side code | Smart contracts |
|
||||
| **Heavy Computation** | Server processing | Decentralized compute platforms |
|
||||
| **Authentication** | Centralized login (e.g., OAuth) | Decentralized identity (e.g., SSI) |
|
||||
| **Communication** | WebSockets on servers | P2P protocols (e.g., libp2p) |
|
||||
|
||||
In a decentralized system, the backend’s role is minimized—smart contracts handle most logic, and other tasks are offloaded to decentralized services or user devices.
|
||||
|
||||
|
@ -38,7 +38,7 @@
|
||||
|
||||
### 6. User Experience (UX)
|
||||
- **Focus**: Intuitive and welcoming UX to drive adoption.
|
||||
- **Tech**: Build the frontend with **Elm** for a reliable, clean interface.
|
||||
- **Tech**: Build the frontend with **Next.js** for a reliable, clean interface.
|
||||
- **Goals**: Easy onboarding and clear feedback submission process.
|
||||
|
||||
### 7. Development Phases
|
||||
@ -49,7 +49,7 @@
|
||||
2. Integrate Privado ID and simpler logins.
|
||||
3. Set up IPFS with Pinata.
|
||||
4. Deploy a subgraph on The Graph.
|
||||
5. Build an Elm frontend.
|
||||
5. Build an Next.js frontend.
|
||||
6. Add off-chain AI and moderation.
|
||||
7. Test end-to-end.
|
||||
- **Phase 2: Production**
|
||||
@ -127,7 +127,7 @@ Here’s a detailed breakdown of the full suite of dependencies, components, ser
|
||||
|
||||
## 6. System Architecture
|
||||
- **Technology**: Microservices Architecture (production), Monolithic Architecture (Proof of Concept)
|
||||
- **Language**: Go (backend services), Elm (frontend)
|
||||
- **Language**: Go (backend services), Next.js (frontend)
|
||||
- **Responsibilities**:
|
||||
- **Identity Service**: Manages ZKP verification and simpler login options for users.
|
||||
- **Feedback Service**: Handles submission, storage, and retrieval of feedback data.
|
||||
@ -137,8 +137,8 @@ Here’s a detailed breakdown of the full suite of dependencies, components, ser
|
||||
---
|
||||
|
||||
## 7. User Experience (UX)
|
||||
- **Technology**: Elm
|
||||
- **Language**: Elm (frontend), CSS/HTML (styling)
|
||||
- **Technology**: Next.js
|
||||
- **Language**: TypeScript (frontend), CSS/HTML (styling)
|
||||
- **Responsibilities**:
|
||||
- Delivers an intuitive, welcoming interface to enhance user engagement.
|
||||
- Streamlines onboarding and feedback submission for ease of use.
|
||||
@ -181,6 +181,6 @@ Here’s a detailed breakdown of the full suite of dependencies, components, ser
|
||||
| Storage | IPFS with Pinata | JS/TS | Cost-efficient data storage |
|
||||
| Indexing | The Graph | GraphQL, AssemblyScript| Efficient data querying |
|
||||
| Analysis | Hugging Face or spaCy | Python | Insightful feedback analysis |
|
||||
| Architecture | Microservices/Monolithic | Go (backend), Elm | Scalable system design |
|
||||
| Frontend | Elm | Elm, CSS/HTML | User-friendly interface |
|
||||
| Architecture | Microservices/Monolithic | Go (backend), Next.js | Scalable system design |
|
||||
| Frontend | Next.js | Next.js, CSS/HTML | User-friendly interface |
|
||||
| Development Tools | Hardhat, Pinata | JS/TS | Streamlined development |
|
||||
|
@ -77,7 +77,7 @@ Discourse’s architecture is designed to be decentralized, scalable, and resili
|
||||
- **Why**: Robust tools for natural language processing and sentiment analysis.
|
||||
- **Role**: Synthesizes Perspectives into Insights off-chain, feeding results back to the blockchain.
|
||||
|
||||
- **Frontend: Elm (Hosted on IPFS)**
|
||||
- **Frontend: Next.js (TypeScript) (Hosted on IPFS)**
|
||||
- **Why**: A reliable, functional programming language paired with decentralized hosting for accessibility and resilience.
|
||||
- **Role**: Provides a user-friendly interface for submitting Perspectives, voting, and viewing Resolutions.
|
||||
|
||||
|
3
ui/prototype/app/contributions/[id]/loading.tsx
Normal file
3
ui/prototype/app/contributions/[id]/loading.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return <div className="container mx-auto p-4">Loading contribution details...</div>
|
||||
}
|
499
ui/prototype/app/contributions/[id]/page.tsx
Normal file
499
ui/prototype/app/contributions/[id]/page.tsx
Normal file
@ -0,0 +1,499 @@
|
||||
"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>
|
||||
)
|
||||
}
|
@ -105,6 +105,8 @@ export default function InsightsDashboard() {
|
||||
const [sortBy, setSortBy] = useState<"votes" | "date">("votes")
|
||||
const { walletConnected } = useWalletStore()
|
||||
const [userVotes, setUserVotes] = useState<Record<number, "yes" | "no" | null>>({})
|
||||
const [pendingToast, setPendingToast] = useState<{ type: "single" | "batch"; voteType?: "yes" | "no"; insightId?: number; batchSize?: number } | null>(null)
|
||||
const lastToastRef = useRef<{ type: string, id?: number, time: number }>({ type: "", time: 0 })
|
||||
|
||||
// Batch voting state
|
||||
const [batchMode, setBatchMode] = useState(false)
|
||||
@ -178,6 +180,11 @@ export default function InsightsDashboard() {
|
||||
return insight
|
||||
}),
|
||||
)
|
||||
|
||||
// Queue toast for individual votes (not batch mode)
|
||||
if (!batchMode) {
|
||||
setPendingToast({ type: "single", voteType, insightId: id })
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle batch mode
|
||||
@ -244,32 +251,66 @@ export default function InsightsDashboard() {
|
||||
|
||||
// Process all votes in the batch
|
||||
const processBatchVotes = () => {
|
||||
const batchSize = Object.keys(selectedInsights).length
|
||||
if (batchSize === 0) return
|
||||
|
||||
// Apply all votes
|
||||
Object.entries(selectedInsights).forEach(([idStr, voteType]) => {
|
||||
const id = Number.parseInt(idStr)
|
||||
handleVote(id, voteType)
|
||||
})
|
||||
|
||||
// Show success toast
|
||||
toast({
|
||||
title: "Votes submitted successfully",
|
||||
description: `You've voted on ${selectedCount} insights.`,
|
||||
action: <ToastAction altText="OK">OK</ToastAction>,
|
||||
})
|
||||
// Queue toast for batch submission
|
||||
setPendingToast({ type: "batch", batchSize })
|
||||
|
||||
// Reset batch state
|
||||
setSelectedInsights({})
|
||||
setIsSubmittingBatch(false)
|
||||
}
|
||||
|
||||
// Clean up interval on unmount
|
||||
// Handle toast notifications
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (countdownRef.current) {
|
||||
clearInterval(countdownRef.current)
|
||||
if (pendingToast) {
|
||||
// Check for duplicate toast prevention (only show if different from last toast or more than 2 seconds passed)
|
||||
const now = Date.now()
|
||||
const isDuplicate =
|
||||
pendingToast.type === lastToastRef.current.type &&
|
||||
(pendingToast.type === "single" ? pendingToast.insightId === lastToastRef.current.id : true) &&
|
||||
now - lastToastRef.current.time < 2000;
|
||||
|
||||
if (!isDuplicate) {
|
||||
if (pendingToast.type === "single") {
|
||||
const insight = insights.find(i => i.id === pendingToast.insightId)
|
||||
toast({
|
||||
title: "Vote submitted",
|
||||
description: `You voted ${pendingToast.voteType} on "${insight?.title}"`,
|
||||
action: <ToastAction altText="OK">OK</ToastAction>,
|
||||
})
|
||||
|
||||
// Update last toast reference
|
||||
lastToastRef.current = {
|
||||
type: "single",
|
||||
id: pendingToast.insightId,
|
||||
time: now
|
||||
}
|
||||
} else if (pendingToast.type === "batch") {
|
||||
toast({
|
||||
title: "Batch votes submitted",
|
||||
description: `Successfully submitted ${pendingToast.batchSize} votes`,
|
||||
action: <ToastAction altText="OK">OK</ToastAction>,
|
||||
})
|
||||
|
||||
// Update last toast reference
|
||||
lastToastRef.current = {
|
||||
type: "batch",
|
||||
time: now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setPendingToast(null)
|
||||
}
|
||||
}, [])
|
||||
}, [pendingToast, insights])
|
||||
|
||||
// Enhanced HoldButton component with improved animation and feedback
|
||||
function HoldButton({
|
||||
@ -294,12 +335,13 @@ export default function InsightsDashboard() {
|
||||
const [isHolding, setIsHolding] = useState(false)
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [completed, setCompleted] = useState(false)
|
||||
const [localVoted, setLocalVoted] = useState(false) // Track local vote state
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const startTimeRef = useRef<number>(0)
|
||||
const animationRef = useRef<number | null>(null)
|
||||
|
||||
// Check if this insight has already been voted on
|
||||
const hasVoted = userVotes[insightId] !== undefined
|
||||
const hasVoted = userVotes[insightId] !== undefined || localVoted
|
||||
const isThisVote = userVotes[insightId] === voteType
|
||||
|
||||
// Reset completed state when the insight changes
|
||||
@ -324,6 +366,7 @@ export default function InsightsDashboard() {
|
||||
setCompleted(true)
|
||||
// Add a small delay before triggering the action for visual feedback
|
||||
timerRef.current = setTimeout(() => {
|
||||
setLocalVoted(true) // Set local vote state immediately
|
||||
onComplete()
|
||||
setIsHolding(false)
|
||||
setProgress(0)
|
||||
|
460
ui/prototype/app/issues/[id]/page.tsx
Normal file
460
ui/prototype/app/issues/[id]/page.tsx
Normal file
@ -0,0 +1,460 @@
|
||||
"use client"
|
||||
|
||||
import { useState, use } from "react"
|
||||
import { ArrowLeft, MessageSquare, ThumbsUp, ThumbsDown, Share2, FileText, Filter } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
|
||||
// Mock data for the issue
|
||||
const mockIssues = {
|
||||
"climate-action": {
|
||||
id: "climate-action",
|
||||
title: "Climate Action Policies",
|
||||
description: "Discussion on policies to address climate change and reduce carbon emissions.",
|
||||
category: "Environmental Policy",
|
||||
dateCreated: "2024-01-10",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 324,
|
||||
insights: 18,
|
||||
contributions: 5,
|
||||
participants: 156
|
||||
}
|
||||
},
|
||||
"education-reform": {
|
||||
id: "education-reform",
|
||||
title: "Education System Reform",
|
||||
description: "Ideas for improving educational outcomes and accessibility for all students.",
|
||||
category: "Education",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 187,
|
||||
insights: 12,
|
||||
contributions: 3,
|
||||
participants: 89
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock perspectives for issues
|
||||
const mockPerspectivesByIssue = {
|
||||
"climate-action": [
|
||||
{
|
||||
id: 1,
|
||||
userId: "user1",
|
||||
userName: "Alex Johnson",
|
||||
userAvatar: "",
|
||||
content: "We should focus on renewable energy investments. Solar and wind power have become more cost-effective and could replace fossil fuels in many regions.",
|
||||
dateSubmitted: "2024-03-01",
|
||||
likes: 45,
|
||||
dislikes: 3
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userId: "user2",
|
||||
userName: "Jamie Smith",
|
||||
userAvatar: "",
|
||||
content: "Carbon pricing is the most efficient way to reduce emissions. It creates market incentives for businesses to innovate and cut their carbon footprint.",
|
||||
dateSubmitted: "2024-02-28",
|
||||
likes: 38,
|
||||
dislikes: 7
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
userId: "user3",
|
||||
userName: "Taylor Reed",
|
||||
userAvatar: "",
|
||||
content: "We need to address transportation emissions through better public transit and EV infrastructure. This sector is a major contributor to greenhouse gases.",
|
||||
dateSubmitted: "2024-02-25",
|
||||
likes: 52,
|
||||
dislikes: 4
|
||||
}
|
||||
],
|
||||
"education-reform": [
|
||||
{
|
||||
id: 1,
|
||||
userId: "user4",
|
||||
userName: "Morgan Lee",
|
||||
userAvatar: "",
|
||||
content: "Teachers need better resources and smaller class sizes to effectively improve student outcomes.",
|
||||
dateSubmitted: "2024-03-02",
|
||||
likes: 62,
|
||||
dislikes: 5
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userId: "user5",
|
||||
userName: "Casey Wilson",
|
||||
userAvatar: "",
|
||||
content: "Access to early childhood education should be a priority as it sets the foundation for all future learning.",
|
||||
dateSubmitted: "2024-02-27",
|
||||
likes: 41,
|
||||
dislikes: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Mock insights for issues
|
||||
const mockInsightsByIssue = {
|
||||
"climate-action": [
|
||||
{
|
||||
id: 1,
|
||||
title: "Renewable Energy Investment",
|
||||
description: "67% of perspectives support increased government funding for renewable energy projects, with solar and wind being the most frequently mentioned technologies.",
|
||||
votes: { yes: 124, no: 18 },
|
||||
status: "consensus", // voting, consensus, rejected
|
||||
perspectives: [1, 5, 8, 12] // Reference to perspective IDs
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Carbon Pricing Mechanism",
|
||||
description: "A majority of users advocate for carbon pricing policies, with 58% specifically mentioning tax incentives for low-emission businesses.",
|
||||
votes: { yes: 98, no: 32 },
|
||||
status: "voting", // voting, consensus, rejected
|
||||
perspectives: [2, 7, 15] // Reference to perspective IDs
|
||||
}
|
||||
],
|
||||
"education-reform": [
|
||||
{
|
||||
id: 1,
|
||||
title: "Teacher Support Systems",
|
||||
description: "78% of perspectives emphasize the need for better resources and support for teachers, including smaller class sizes and professional development.",
|
||||
votes: { yes: 112, no: 15 },
|
||||
status: "consensus",
|
||||
perspectives: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Early Childhood Education Access",
|
||||
description: "64% of perspectives advocate for universal access to early childhood education to establish a strong foundation for learning.",
|
||||
votes: { yes: 87, no: 34 },
|
||||
status: "voting",
|
||||
perspectives: [2, 8, 10]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default function IssueDetails({ params }: { params: Promise<{ id: string }> }) {
|
||||
// Properly unwrap params using React.use()
|
||||
const { id: issueId } = use(params)
|
||||
const [activeTab, setActiveTab] = useState("overview")
|
||||
const [commentText, setCommentText] = useState("")
|
||||
|
||||
// Get the issue data from our mock data
|
||||
const issue = mockIssues[issueId as keyof typeof mockIssues] || mockIssues["climate-action"]
|
||||
const perspectives = mockPerspectivesByIssue[issueId as keyof typeof mockPerspectivesByIssue] || []
|
||||
const insights = mockInsightsByIssue[issueId as keyof typeof mockInsightsByIssue] || []
|
||||
|
||||
const handleSubmitComment = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (commentText.trim()) {
|
||||
// In a real app, submit the comment to the backend
|
||||
console.log("Submitting comment:", commentText)
|
||||
setCommentText("")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-6">
|
||||
<Button variant="ghost" size="sm" asChild className="mb-4">
|
||||
<Link href="/issues">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Issues
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{issue.title}</h1>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Badge variant="outline">{issue.category}</Badge>
|
||||
<Badge variant="secondary" className="capitalize">{issue.status}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-4 md:mt-0">
|
||||
<Button variant="outline" size="sm" className="gap-1">
|
||||
<Share2 className="h-4 w-4" />
|
||||
Share
|
||||
</Button>
|
||||
<Button variant="default" size="sm" asChild>
|
||||
<Link href={`/submit?issue=${issueId}`}>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Add Perspective
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mb-6">{issue.description}</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{issue.metrics.perspectives}</div>
|
||||
<p className="text-sm text-muted-foreground">Perspectives</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{issue.metrics.insights}</div>
|
||||
<p className="text-sm text-muted-foreground">Insights</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{issue.metrics.contributions}</div>
|
||||
<p className="text-sm text-muted-foreground">Contributions</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{issue.metrics.participants}</div>
|
||||
<p className="text-sm text-muted-foreground">Participants</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="perspectives">Perspectives</TabsTrigger>
|
||||
<TabsTrigger value="insights">Insights</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="pt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Issue Summary</CardTitle>
|
||||
<CardDescription>Key information and progress on this issue</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="font-medium mb-2">Progress</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>Perspectives Gathering</span>
|
||||
<span className="text-muted-foreground">75%</span>
|
||||
</div>
|
||||
<Progress value={75} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>Insight Formation</span>
|
||||
<span className="text-muted-foreground">45%</span>
|
||||
</div>
|
||||
<Progress value={45} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>Contribution Validation</span>
|
||||
<span className="text-muted-foreground">20%</span>
|
||||
</div>
|
||||
<Progress value={20} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div>
|
||||
<h3 className="font-medium mb-2">Timeline</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex">
|
||||
<div className="mr-4 mt-0.5">
|
||||
<Badge variant="secondary" className="h-8 w-8 rounded-full flex items-center justify-center p-0">1</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium">Perspectives Collection</h4>
|
||||
<p className="text-sm text-muted-foreground">In progress - Ends Apr 15, 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="mr-4 mt-0.5">
|
||||
<Badge variant="outline" className="h-8 w-8 rounded-full flex items-center justify-center p-0">2</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium">Insight Voting</h4>
|
||||
<p className="text-sm text-muted-foreground">Apr 16 - May 1, 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="mr-4 mt-0.5">
|
||||
<Badge variant="outline" className="h-8 w-8 rounded-full flex items-center justify-center p-0">3</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium">Resolution Formation</h4>
|
||||
<p className="text-sm text-muted-foreground">May 2 - May 15, 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="perspectives" className="pt-6">
|
||||
<div className="flex justify-between mb-4">
|
||||
<h2 className="text-xl font-bold">Recent Perspectives</h2>
|
||||
<div className="flex gap-2">
|
||||
<Select defaultValue="recent">
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="recent">Most Recent</SelectItem>
|
||||
<SelectItem value="popular">Most Popular</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{perspectives.map((perspective) => (
|
||||
<Card key={perspective.id}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Avatar className="h-8 w-8 mr-2">
|
||||
<AvatarImage src={perspective.userAvatar} />
|
||||
<AvatarFallback>{perspective.userName.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium">{perspective.userName}</p>
|
||||
<p className="text-xs text-muted-foreground">{perspective.dateSubmitted}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>{perspective.content}</p>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t pt-4 flex justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" className="gap-1">
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
{perspective.likes}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="gap-1">
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
{perspective.dislikes}
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm">Reply</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Add Your Perspective</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmitComment}>
|
||||
<Textarea
|
||||
placeholder="Share your thoughts on this issue..."
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button type="submit" disabled={!commentText.trim()}>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Submit Perspective
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="insights" className="pt-6">
|
||||
<div className="flex justify-between mb-4">
|
||||
<h2 className="text-xl font-bold">Emerging Insights</h2>
|
||||
<Button variant="outline" size="sm">
|
||||
<Filter className="mr-2 h-4 w-4" />
|
||||
Filter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{insights.map((insight) => (
|
||||
<Card key={insight.id}>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between">
|
||||
<CardTitle>{insight.title}</CardTitle>
|
||||
<Badge
|
||||
variant={insight.status === "consensus" ? "default" : "outline"}
|
||||
className={insight.status === "consensus" ? "bg-green-500" : ""}
|
||||
>
|
||||
{insight.status === "consensus" ? "Consensus Reached" : "Voting In Progress"}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4">{insight.description}</p>
|
||||
<div className="rounded-md bg-muted p-4">
|
||||
<div className="text-sm text-muted-foreground mb-2">Voting Progress</div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>
|
||||
<ThumbsUp className="h-3 w-3 inline mr-1" />
|
||||
{insight.votes.yes} Yes
|
||||
</span>
|
||||
<span>
|
||||
<ThumbsDown className="h-3 w-3 inline mr-1" />
|
||||
{insight.votes.no} No
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={(insight.votes.yes / (insight.votes.yes + insight.votes.no)) * 100}
|
||||
className="h-2"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t pt-4 flex justify-between">
|
||||
<div>
|
||||
<Badge variant="outline" className="mr-2">
|
||||
Based on {insight.perspectives.length} perspectives
|
||||
</Badge>
|
||||
</div>
|
||||
{insight.status === "voting" && (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="gap-1">
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
Vote No
|
||||
</Button>
|
||||
<Button variant="default" size="sm" className="gap-1">
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
Vote Yes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
250
ui/prototype/app/issues/page.tsx
Normal file
250
ui/prototype/app/issues/page.tsx
Normal file
@ -0,0 +1,250 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { Search, Filter, ArrowUpDown, Plus } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
|
||||
// Mock data for issues
|
||||
const mockIssues = [
|
||||
{
|
||||
id: "climate-action",
|
||||
title: "Climate Action Policies",
|
||||
description: "Discussion on policies to address climate change and reduce carbon emissions.",
|
||||
category: "Environmental Policy",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 324,
|
||||
insights: 18,
|
||||
contributions: 5
|
||||
},
|
||||
dateCreated: "2024-01-10"
|
||||
},
|
||||
{
|
||||
id: "education-reform",
|
||||
title: "Education System Reform",
|
||||
description: "Ideas for improving educational outcomes and accessibility for all students.",
|
||||
category: "Education",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 187,
|
||||
insights: 12,
|
||||
contributions: 3
|
||||
},
|
||||
dateCreated: "2024-01-22"
|
||||
},
|
||||
{
|
||||
id: "healthcare-access",
|
||||
title: "Healthcare Accessibility",
|
||||
description: "Solutions to make healthcare more affordable and accessible for everyone.",
|
||||
category: "Healthcare",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 256,
|
||||
insights: 15,
|
||||
contributions: 4
|
||||
},
|
||||
dateCreated: "2024-02-05"
|
||||
},
|
||||
{
|
||||
id: "housing-affordability",
|
||||
title: "Housing Affordability Crisis",
|
||||
description: "Addressing the growing housing affordability crisis in urban areas.",
|
||||
category: "Infrastructure",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 210,
|
||||
insights: 8,
|
||||
contributions: 2
|
||||
},
|
||||
dateCreated: "2024-02-15"
|
||||
},
|
||||
{
|
||||
id: "digital-privacy",
|
||||
title: "Digital Privacy Regulations",
|
||||
description: "Balancing innovation with personal privacy in the digital age.",
|
||||
category: "Technology",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 156,
|
||||
insights: 7,
|
||||
contributions: 1
|
||||
},
|
||||
dateCreated: "2024-02-28"
|
||||
}
|
||||
]
|
||||
|
||||
// Categories for filtering
|
||||
const categories = [
|
||||
"All Categories",
|
||||
"Environmental Policy",
|
||||
"Education",
|
||||
"Healthcare",
|
||||
"Infrastructure",
|
||||
"Technology",
|
||||
"Economy",
|
||||
"Social Services"
|
||||
]
|
||||
|
||||
export default function IssuesPage() {
|
||||
const [selectedTab, setSelectedTab] = useState("all")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedCategory, setSelectedCategory] = useState("All Categories")
|
||||
const [sortBy, setSortBy] = useState("latest")
|
||||
|
||||
// Filter issues based on search, category, and tab
|
||||
const filteredIssues = mockIssues.filter(issue => {
|
||||
// Search filter
|
||||
const matchesSearch = searchQuery
|
||||
? issue.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
issue.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
: true
|
||||
|
||||
// Category filter
|
||||
const matchesCategory = selectedCategory === "All Categories" || issue.category === selectedCategory
|
||||
|
||||
// Tab filter (all, trending, etc.) - simplified for demo
|
||||
const matchesTab = selectedTab === "all" ||
|
||||
(selectedTab === "trending" && issue.metrics.perspectives > 200)
|
||||
|
||||
return matchesSearch && matchesCategory && matchesTab
|
||||
})
|
||||
|
||||
// Sort issues
|
||||
const sortedIssues = [...filteredIssues].sort((a, b) => {
|
||||
if (sortBy === "latest") {
|
||||
return new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime()
|
||||
} else if (sortBy === "popular") {
|
||||
return b.metrics.perspectives - a.metrics.perspectives
|
||||
} else if (sortBy === "insights") {
|
||||
return b.metrics.insights - a.metrics.insights
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Issues</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Browse and contribute to active policy discussions
|
||||
</p>
|
||||
</div>
|
||||
<Button className="mt-4 sm:mt-0" asChild>
|
||||
<Link href="/issues/propose">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Propose New Issue
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Filters and Search */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search issues..."
|
||||
className="pl-8"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Select
|
||||
value={selectedCategory}
|
||||
onValueChange={setSelectedCategory}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[180px]">
|
||||
<SelectValue placeholder="Category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category) => (
|
||||
<SelectItem key={category} value={category}>
|
||||
{category}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={sortBy}
|
||||
onValueChange={setSortBy}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[180px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="latest">Latest</SelectItem>
|
||||
<SelectItem value="popular">Most Perspectives</SelectItem>
|
||||
<SelectItem value="insights">Most Insights</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab} className="w-full">
|
||||
<TabsList className="grid w-full sm:w-auto sm:inline-grid grid-cols-3 sm:grid-cols-3">
|
||||
<TabsTrigger value="all">All Issues</TabsTrigger>
|
||||
<TabsTrigger value="trending">Trending</TabsTrigger>
|
||||
<TabsTrigger value="following">Following</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* Results */}
|
||||
{sortedIssues.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<h3 className="text-lg font-medium">No issues found</h3>
|
||||
<p className="text-muted-foreground mt-2">Try adjusting your filters or search terms</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{sortedIssues.map((issue) => (
|
||||
<Link key={issue.id} href={`/issues/${issue.id}`} className="transition-transform hover:scale-[1.01]">
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle className="line-clamp-2">{issue.title}</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
<Badge variant="outline">{issue.category}</Badge>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow">
|
||||
<p className="text-muted-foreground line-clamp-3">{issue.description}</p>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t pt-4">
|
||||
<div className="w-full flex justify-between text-sm text-muted-foreground">
|
||||
<div className="flex gap-4">
|
||||
<div>{issue.metrics.perspectives} Perspectives</div>
|
||||
<div>{issue.metrics.insights} Insights</div>
|
||||
</div>
|
||||
<div>
|
||||
{new Date(issue.dateCreated).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
293
ui/prototype/app/issues/propose/page.tsx
Normal file
293
ui/prototype/app/issues/propose/page.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { ArrowLeft, FileText, HelpCircle, AlertTriangle, CheckCircle, Loader2 } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { useWalletStore } from "@/lib/wallet-store"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
// Categories for selection
|
||||
const categories = [
|
||||
"Environmental Policy",
|
||||
"Education",
|
||||
"Healthcare",
|
||||
"Infrastructure",
|
||||
"Technology",
|
||||
"Economy",
|
||||
"Social Services",
|
||||
"Other"
|
||||
]
|
||||
|
||||
export default function ProposeIssuePage() {
|
||||
const { walletConnected } = useWalletStore()
|
||||
const { toast } = useToast()
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: "",
|
||||
description: "",
|
||||
category: "",
|
||||
details: "",
|
||||
keywords: "",
|
||||
publicProposal: true
|
||||
})
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
const [proposalId, setProposalId] = useState("")
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target
|
||||
setFormData(prev => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
const handleSelectChange = (name: string) => (value: string) => {
|
||||
setFormData(prev => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
const handleSwitchChange = (checked: boolean) => {
|
||||
setFormData(prev => ({ ...prev, publicProposal: checked }))
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!walletConnected) {
|
||||
toast({
|
||||
title: "Wallet not connected",
|
||||
description: "Please connect your wallet to propose a new issue.",
|
||||
variant: "destructive"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
// This would submit to the blockchain in production
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)) // Simulate network delay
|
||||
|
||||
// Generate a random ID for the demo
|
||||
const id = Math.random().toString(36).substring(2, 10)
|
||||
setProposalId(id)
|
||||
setIsSubmitted(true)
|
||||
} catch (error) {
|
||||
console.error("Error submitting proposal:", error)
|
||||
toast({
|
||||
title: "Submission failed",
|
||||
description: "There was an error submitting your proposal. Please try again.",
|
||||
variant: "destructive"
|
||||
})
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-8 flex flex-col items-center text-center">
|
||||
<CheckCircle className="h-16 w-16 text-green-500 mb-4" />
|
||||
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">Issue Proposed Successfully!</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
Your issue proposal has been submitted and is now pending review.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Proposal ID: {proposalId}
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-col sm:flex-row gap-4">
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/issues">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Issues
|
||||
</Link>
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
setIsSubmitted(false)
|
||||
setFormData({
|
||||
title: "",
|
||||
description: "",
|
||||
category: "",
|
||||
details: "",
|
||||
keywords: "",
|
||||
publicProposal: true
|
||||
})
|
||||
}}>
|
||||
Propose Another Issue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<Button variant="ghost" size="sm" asChild className="mb-4">
|
||||
<Link href="/issues">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Issues
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">Propose a New Issue</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
Suggest a topic for community discussion and potential policy recommendation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!walletConnected && (
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Wallet not connected</AlertTitle>
|
||||
<AlertDescription>
|
||||
You need to connect your wallet before proposing an issue.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Issue Information</CardTitle>
|
||||
<CardDescription>
|
||||
Provide details about the issue you want to propose for discussion.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Issue Title</Label>
|
||||
<Input
|
||||
id="title"
|
||||
name="title"
|
||||
placeholder="Enter a clear, descriptive title"
|
||||
value={formData.title}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Brief Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
placeholder="Provide a short summary of the issue (1-2 sentences)"
|
||||
value={formData.description}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This will appear in issue listings and search results.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Select
|
||||
value={formData.category}
|
||||
onValueChange={handleSelectChange("category")}
|
||||
required
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category) => (
|
||||
<SelectItem key={category} value={category}>
|
||||
{category}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="details">Detailed Description</Label>
|
||||
<Textarea
|
||||
id="details"
|
||||
name="details"
|
||||
placeholder="Provide comprehensive details about the issue, including background, significance, and potential impacts"
|
||||
className="min-h-[200px]"
|
||||
value={formData.details}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="keywords">Keywords (Optional)</Label>
|
||||
<Input
|
||||
id="keywords"
|
||||
name="keywords"
|
||||
placeholder="Enter keywords separated by commas (e.g., climate, renewable, energy)"
|
||||
value={formData.keywords}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="public-proposal"
|
||||
checked={formData.publicProposal}
|
||||
onCheckedChange={handleSwitchChange}
|
||||
/>
|
||||
<Label htmlFor="public-proposal">Make this proposal public immediately</Label>
|
||||
<Button variant="ghost" size="icon" type="button" className="h-8 w-8">
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t pt-6 flex flex-col sm:flex-row sm:justify-between gap-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
All proposals are subject to community moderation
|
||||
</p>
|
||||
<Button type="submit" disabled={isSubmitting || !walletConnected}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Submitting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
Submit Proposal
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
|
||||
<Card className="mt-8">
|
||||
<CardHeader>
|
||||
<CardTitle>Proposal Guidelines</CardTitle>
|
||||
<CardDescription>Tips for submitting an effective issue proposal</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="list-disc space-y-2 pl-4 text-muted-foreground">
|
||||
<li>Focus on issues that affect a significant portion of the community</li>
|
||||
<li>Be clear and specific about the issue you're addressing</li>
|
||||
<li>Provide objective information rather than personal opinions</li>
|
||||
<li>Consider including data or research to support your proposal</li>
|
||||
<li>Avoid duplicate issues - search first to see if your topic already exists</li>
|
||||
<li>Maintain a constructive tone that encourages productive discussion</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
3
ui/prototype/app/resolutions/[id]/loading.tsx
Normal file
3
ui/prototype/app/resolutions/[id]/loading.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return <div className="container mx-auto p-4">Loading resolution details...</div>
|
||||
}
|
529
ui/prototype/app/resolutions/[id]/page.tsx
Normal file
529
ui/prototype/app/resolutions/[id]/page.tsx
Normal file
@ -0,0 +1,529 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { use } from "react"
|
||||
import {
|
||||
ArrowLeft,
|
||||
FileText,
|
||||
Download,
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
PieChart,
|
||||
Users,
|
||||
LucideIcon,
|
||||
AlertCircle,
|
||||
BarChart3,
|
||||
Clock,
|
||||
FileBarChart,
|
||||
Share2,
|
||||
Bookmark
|
||||
} 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 {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion"
|
||||
|
||||
// Mock data for resolutions with detailed content
|
||||
const mockResolutions = [
|
||||
{
|
||||
id: 12,
|
||||
title: "Education and Community Health Improvements",
|
||||
description: "A comprehensive set of recommendations for improving education access and community health services",
|
||||
summary: "This resolution addresses critical gaps in education and healthcare access by recommending the creation of free after-school programs for K-8 students and establishing community health clinics in underserved areas. These measures aim to improve educational outcomes, provide structured environments for children after school hours, and increase healthcare access for vulnerable populations.",
|
||||
date: "2024-02-15",
|
||||
contributions: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "Teacher Professional Development",
|
||||
description: "Increase funding for teacher training programs focused on inclusive education",
|
||||
category: "Education",
|
||||
votes: { yes: 245, no: 52 },
|
||||
perspectives: 31,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: "Preventive Health Education",
|
||||
description: "Implement community-based health education programs focused on prevention",
|
||||
category: "Healthcare",
|
||||
votes: { yes: 231, no: 38 },
|
||||
perspectives: 29,
|
||||
},
|
||||
],
|
||||
categories: ["Education", "Healthcare"],
|
||||
status: "active",
|
||||
timeline: [
|
||||
{
|
||||
date: "2023-09-01",
|
||||
event: "Perspective Collection Started",
|
||||
description: "Community members began submitting perspectives on education and healthcare needs"
|
||||
},
|
||||
{
|
||||
date: "2023-10-15",
|
||||
event: "Insights Generation",
|
||||
description: "AI-generated insights were derived from collected perspectives"
|
||||
},
|
||||
{
|
||||
date: "2023-11-15",
|
||||
event: "First Contribution Validated",
|
||||
description: "After-School Programs contribution reached consensus"
|
||||
},
|
||||
{
|
||||
date: "2023-12-03",
|
||||
event: "Second Contribution Validated",
|
||||
description: "Community Health Clinics contribution reached consensus"
|
||||
},
|
||||
{
|
||||
date: "2024-01-20",
|
||||
event: "Resolution Draft",
|
||||
description: "Initial resolution draft based on validated contributions"
|
||||
},
|
||||
{
|
||||
date: "2024-02-15",
|
||||
event: "Resolution Published",
|
||||
description: "Final resolution published after community review"
|
||||
}
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
id: "1",
|
||||
title: "Background and Context",
|
||||
content: "Recent community feedback has highlighted significant gaps in both educational support services and healthcare access, particularly in underserved neighborhoods. Data indicates that approximately 45% of K-8 students lack supervision during after-school hours, contributing to concerning trends in academic performance and youth engagement. Additionally, 38% of residents in priority neighborhoods report delaying healthcare due to lack of affordable and accessible services."
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Recommendations",
|
||||
content: "Based on community consensus, this resolution recommends: 1) Establishing free after-school programs in all public elementary and middle schools, with priority implementation in underserved areas; 2) Creating walk-in community health clinics with sliding scale fee structures in identified healthcare deserts; 3) Increasing funding for teacher professional development programs focused on inclusive education; and 4) Implementing community-based preventive health education programs.",
|
||||
subsections: [
|
||||
{
|
||||
id: "2.1",
|
||||
title: "Education Initiatives",
|
||||
content: "After-school programs should operate from 3:00-6:00 PM on school days, offering tutoring, enrichment activities, and physical recreation. Teacher professional development programs should prioritize training in inclusive education practices and trauma-informed approaches."
|
||||
},
|
||||
{
|
||||
id: "2.2",
|
||||
title: "Healthcare Initiatives",
|
||||
content: "Community health clinics should offer basic primary care, preventive services, and health education. Sliding scale fees should ensure that no resident is denied care due to inability to pay, with free services available to those meeting income requirements."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Implementation Timeline",
|
||||
content: "Phase 1 (Immediate): Begin planning and resource allocation for both education and healthcare initiatives; Phase 2 (6-12 months): Launch pilot programs in highest-need areas; Phase 3 (12-24 months): Expand to remaining target areas; Phase 4 (Ongoing): Continuous evaluation and improvement."
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "Resource Requirements",
|
||||
content: "Education Initiatives: Estimated annual budget of $4.2M for staffing, materials, and facility usage; Healthcare Initiatives: Estimated annual budget of $5.8M for facilities, medical staff, and supplies. Recommended funding sources include reallocation from existing programs, grant opportunities, and public-private partnerships."
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
title: "Expected Outcomes",
|
||||
content: "1) Improved academic performance and decreased truancy in K-8 students; 2) Reduced emergency room usage for non-emergency conditions; 3) Earlier detection and treatment of health conditions; 4) Increased community satisfaction with public services; 5) Long-term reduction in costs associated with preventable health conditions and educational remediation."
|
||||
}
|
||||
],
|
||||
impactMetrics: {
|
||||
estimatedBeneficiaries: 25000,
|
||||
costEfficiency: "High",
|
||||
implementationComplexity: "Medium",
|
||||
timeToImpact: "6-12 months",
|
||||
sustainabilityScore: 8.2,
|
||||
},
|
||||
stakeholders: ["School District", "Department of Health", "Community Organizations", "Parent Associations", "Healthcare Providers"]
|
||||
},
|
||||
// Other resolutions would be defined here
|
||||
]
|
||||
|
||||
export default function ResolutionPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
// Properly handle the params with React.use()
|
||||
const { id } = use(params)
|
||||
const resolutionId = parseInt(id)
|
||||
const [activeTab, setActiveTab] = useState("overview")
|
||||
|
||||
// Find the resolution from mock data
|
||||
const resolution = mockResolutions.find(r => r.id === resolutionId)
|
||||
|
||||
if (!resolution) {
|
||||
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">Resolution Not Found</h1>
|
||||
<p className="text-muted-foreground mb-8">The resolution you're looking for doesn't exist or has been removed.</p>
|
||||
<Button asChild>
|
||||
<Link href="/resolutions">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Return to Resolutions
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Format date for display
|
||||
const formattedDate = new Date(resolution.date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
|
||||
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="/resolutions">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Resolutions
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{resolution.categories.map(category => (
|
||||
<Badge key={category} className="text-xs">{category}</Badge>
|
||||
))}
|
||||
<Badge variant="outline" className="text-xs">ID: {resolution.id}</Badge>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold flex items-center gap-2">
|
||||
<FileText className="h-6 w-6 text-primary" />
|
||||
Resolution #{resolution.id}
|
||||
</h1>
|
||||
<h2 className="text-xl font-semibold mt-2">{resolution.title}</h2>
|
||||
<p className="mt-2 text-muted-foreground">{resolution.description}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-start md:items-end">
|
||||
<Badge variant={resolution.status === "active" ? "default" : "secondary"} className="mb-2">
|
||||
{resolution.status === "active" ? "Active" : "Archived"}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<Calendar className="h-4 w-4 mr-1 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Published: </span>
|
||||
<span>{formattedDate}</span>
|
||||
</div>
|
||||
<Button className="mt-2" size="sm">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download PDF
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
<Button variant="outline" size="sm">
|
||||
<Share2 className="mr-2 h-4 w-4" />
|
||||
Share
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Bookmark className="mr-2 h-4 w-4" />
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="overview" value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="content">Full Content</TabsTrigger>
|
||||
<TabsTrigger value="contributions">Contributions ({resolution.contributions.length})</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Overview Tab */}
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Executive Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm">{resolution.summary}</p>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 text-center">
|
||||
<StatCard
|
||||
icon={Users}
|
||||
label="Beneficiaries"
|
||||
value={resolution.impactMetrics.estimatedBeneficiaries.toLocaleString()}
|
||||
sublabel="estimated impact"
|
||||
/>
|
||||
<StatCard
|
||||
icon={CheckCircle}
|
||||
label="Contributions"
|
||||
value={resolution.contributions.length}
|
||||
sublabel="consensus-based"
|
||||
/>
|
||||
<StatCard
|
||||
icon={BarChart3}
|
||||
label="Cost Efficiency"
|
||||
value={resolution.impactMetrics.costEfficiency}
|
||||
sublabel="ROI rating"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Clock}
|
||||
label="Time to Impact"
|
||||
value={resolution.impactMetrics.timeToImpact}
|
||||
sublabel="expected timeline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<h3 className="font-medium text-lg mb-4">Key Stakeholders</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{resolution.stakeholders.map((stakeholder, index) => (
|
||||
<Badge key={index} variant="outline" className="text-sm py-1">
|
||||
{stakeholder}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Timeline</CardTitle>
|
||||
<CardDescription>Key milestones in the resolution development</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative">
|
||||
{resolution.timeline.map((item, index) => (
|
||||
<div key={index} className="mb-6 flex gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="h-8 w-8 rounded-full bg-primary/20 flex items-center justify-center text-primary">
|
||||
{index === 0 ?
|
||||
<Users className="h-4 w-4" /> :
|
||||
index === resolution.timeline.length - 1 ?
|
||||
<CheckCircle className="h-4 w-4" /> :
|
||||
<FileBarChart className="h-4 w-4" />
|
||||
}
|
||||
</div>
|
||||
{index < resolution.timeline.length - 1 && (
|
||||
<div className="w-px h-full bg-border my-1"></div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{item.event}</p>
|
||||
<p className="text-sm text-muted-foreground">{item.description}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{new Date(item.date).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Impact Assessment</CardTitle>
|
||||
<CardDescription>Projected outcomes and metrics</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">Implementation Complexity</h4>
|
||||
<div className="flex items-center mt-2">
|
||||
<Badge variant={
|
||||
resolution.impactMetrics.implementationComplexity === "Low" ? "outline" :
|
||||
resolution.impactMetrics.implementationComplexity === "Medium" ? "secondary" :
|
||||
"destructive"
|
||||
}>
|
||||
{resolution.impactMetrics.implementationComplexity}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">Sustainability Score</h4>
|
||||
<div className="mt-2 relative pt-1">
|
||||
<div className="flex mb-2 items-center justify-between">
|
||||
<div>
|
||||
<span className="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full bg-green-200 text-green-800">
|
||||
{resolution.impactMetrics.sustainabilityScore}/10
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-hidden h-2 mb-4 text-xs flex rounded bg-muted">
|
||||
<div style={{ width: `${resolution.impactMetrics.sustainabilityScore * 10}%` }}
|
||||
className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-green-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Measures long-term viability and environmental impact
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<h4 className="text-sm font-medium mb-2">Key Outcomes</h4>
|
||||
<ul className="text-sm space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500 mt-0.5" />
|
||||
<span>Improved academic performance and decreased truancy</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500 mt-0.5" />
|
||||
<span>Reduced emergency room usage for non-emergency conditions</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500 mt-0.5" />
|
||||
<span>Earlier detection and treatment of health conditions</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Content Tab */}
|
||||
<TabsContent value="content" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Full Resolution Text</CardTitle>
|
||||
<CardDescription>
|
||||
Resolution #{resolution.id}: {resolution.title}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{resolution.sections.map((section) => (
|
||||
<AccordionItem key={section.id} value={section.id}>
|
||||
<AccordionTrigger className="font-medium">
|
||||
{section.id}. {section.title}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="text-sm space-y-4">
|
||||
<p>{section.content}</p>
|
||||
|
||||
{section.subsections && section.subsections.length > 0 && (
|
||||
<div className="pl-4 border-l-2 border-muted mt-4 space-y-4">
|
||||
{section.subsections.map((subsection) => (
|
||||
<div key={subsection.id}>
|
||||
<h4 className="font-medium">{subsection.id} {subsection.title}</h4>
|
||||
<p className="mt-2">{subsection.content}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Contributions Tab */}
|
||||
<TabsContent value="contributions" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contributing Insights</CardTitle>
|
||||
<CardDescription>
|
||||
Consensus-reached contributions that formed this resolution
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{resolution.contributions.map((contribution) => {
|
||||
const approvalPercentage = Math.round(
|
||||
(contribution.votes.yes / (contribution.votes.yes + contribution.votes.no)) * 100
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={contribution.id} className="border rounded-lg p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="text-xs">{contribution.category}</Badge>
|
||||
<Badge variant="outline" className="text-xs">ID: {contribution.id}</Badge>
|
||||
</div>
|
||||
<h3 className="font-medium mt-2">{contribution.title}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{contribution.description}</p>
|
||||
</div>
|
||||
<div className="text-center min-w-[80px]">
|
||||
<div className="h-16 w-16 rounded-full border-4 border-green-500 flex items-center justify-center mx-auto">
|
||||
<span className="text-lg font-bold">{approvalPercentage}%</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Consensus</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Based on </span>
|
||||
<span className="font-medium">{contribution.perspectives} perspectives</span>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" asChild>
|
||||
<Link href={`/contributions/${contribution.id}`}>
|
||||
View Details
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12 flex justify-center">
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/resolutions">Return to All Resolutions</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Stat card component
|
||||
function StatCard({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
sublabel
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
value: string | number;
|
||||
sublabel: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="p-4 border rounded-lg">
|
||||
<div className="flex justify-center mb-2">
|
||||
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold">{value}</h3>
|
||||
<p className="font-medium text-sm">{label}</p>
|
||||
<p className="text-xs text-muted-foreground">{sublabel}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,156 +1,325 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { CheckCircle, Loader2 } from "lucide-react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { FileText, Upload, CheckCircle, Loader2, ArrowLeft } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { ConnectWalletButton } from "@/components/connect-wallet-button"
|
||||
import { useWalletStore } from "@/lib/wallet-store"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
|
||||
export default function SubmitPerspective() {
|
||||
// Mock issues data for the dropdown
|
||||
const mockIssues = [
|
||||
{
|
||||
id: "climate-action",
|
||||
title: "Climate Action Policies",
|
||||
category: "Environmental Policy"
|
||||
},
|
||||
{
|
||||
id: "education-reform",
|
||||
title: "Education System Reform",
|
||||
category: "Education"
|
||||
},
|
||||
{
|
||||
id: "healthcare-access",
|
||||
title: "Healthcare Accessibility",
|
||||
category: "Healthcare"
|
||||
},
|
||||
{
|
||||
id: "housing-affordability",
|
||||
title: "Housing Affordability Crisis",
|
||||
category: "Infrastructure"
|
||||
},
|
||||
{
|
||||
id: "digital-privacy",
|
||||
title: "Digital Privacy Regulations",
|
||||
category: "Technology"
|
||||
}
|
||||
]
|
||||
|
||||
export default function SubmitPage() {
|
||||
const { walletConnected } = useWalletStore()
|
||||
const searchParams = useSearchParams()
|
||||
const issueParam = searchParams.get('issue')
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
const [ipfsHash, setIpfsHash] = useState("")
|
||||
const [selectedIssue, setSelectedIssue] = useState<string | null>(null)
|
||||
const [title, setTitle] = useState("")
|
||||
const [content, setContent] = useState("")
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
// Set the selected issue from URL parameter if available
|
||||
useEffect(() => {
|
||||
if (issueParam) {
|
||||
setSelectedIssue(issueParam)
|
||||
}
|
||||
}, [issueParam])
|
||||
|
||||
// Get the selected issue details
|
||||
const selectedIssueDetails = selectedIssue ?
|
||||
mockIssues.find(issue => issue.id === selectedIssue) : null
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!walletConnected) return
|
||||
if (!walletConnected) {
|
||||
// Handle wallet connection requirement
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedIssue) {
|
||||
// Issue selection is required
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
// Simulate IPFS upload
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false)
|
||||
try {
|
||||
// TODO: Implement IPFS upload and blockchain submission
|
||||
// For now, simulate a successful submission
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
setIpfsHash("QmTest123...")
|
||||
setIsSubmitted(true)
|
||||
setIpfsHash("QmZ9Ld1SbXfQUvYfTxDKnDUGBJ7R7Vhj2KxJmySCa5TMv3")
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
console.error("Failed to submit perspective:", error)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!walletConnected) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mx-auto max-w-3xl text-center">
|
||||
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">Connect Your Wallet</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
Please connect your wallet to submit a perspective.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mx-auto max-w-3xl text-center">
|
||||
<div className="mb-4 flex justify-center">
|
||||
<CheckCircle className="h-12 w-12 text-green-500" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">Perspective Submitted!</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
Your perspective has been successfully submitted and stored on IPFS.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
IPFS Hash: {ipfsHash}
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col sm:flex-row justify-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsSubmitted(false)
|
||||
setTitle("")
|
||||
setContent("")
|
||||
}}
|
||||
>
|
||||
Submit Another Perspective
|
||||
</Button>
|
||||
{selectedIssue && (
|
||||
<Button asChild>
|
||||
<Link href={`/issues/${selectedIssue}`}>
|
||||
View Issue
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
{selectedIssue && (
|
||||
<Button variant="ghost" size="sm" asChild className="mb-4">
|
||||
<Link href={`/issues/${selectedIssue}`}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Issue
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-3xl font-bold">Submit Your Perspective</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Share your thoughts on issues that matter to you and your community.
|
||||
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">Submit Your Perspective</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
Share your thoughts on important issues. Your perspective helps build a better future for everyone.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isSubmitted ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-primary">
|
||||
<CheckCircle className="h-5 w-5" />
|
||||
Perspective Submitted Successfully
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Your perspective has been recorded on the blockchain and stored on IPFS.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md bg-muted p-4">
|
||||
<p className="text-sm font-medium">IPFS Hash:</p>
|
||||
<p className="mt-1 break-all font-mono text-xs">{ipfsHash}</p>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-muted-foreground">
|
||||
Your perspective will be synthesized with others to create insights that the community can vote on.
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-4 sm:flex-row">
|
||||
<Button variant="outline" className="w-full sm:w-auto" onClick={() => setIsSubmitted(false)}>
|
||||
Submit Another Perspective
|
||||
</Button>
|
||||
<Button className="w-full sm:w-auto" asChild>
|
||||
<a href={`/insights`}>View Insights Dashboard</a>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Share Your Perspective</CardTitle>
|
||||
<CardDescription>
|
||||
Your input will help shape policies and decisions that affect your community.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!walletConnected ? (
|
||||
<div className="flex flex-col items-center gap-4 py-8">
|
||||
<p className="text-center text-muted-foreground">Connect your wallet to submit your perspective</p>
|
||||
<ConnectWalletButton />
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="issue">Select Issue</Label>
|
||||
<Select required>
|
||||
<SelectTrigger id="issue">
|
||||
<SelectValue placeholder="Select an issue" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="environment">Environmental Policy</SelectItem>
|
||||
<SelectItem value="infrastructure">Infrastructure</SelectItem>
|
||||
<SelectItem value="education">Education</SelectItem>
|
||||
<SelectItem value="healthcare">Healthcare</SelectItem>
|
||||
<SelectItem value="housing">Housing</SelectItem>
|
||||
<SelectItem value="other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>New Perspective</CardTitle>
|
||||
<CardDescription>
|
||||
Fill out the form below to submit your perspective. Be specific and constructive in your feedback.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Issue Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="issue" className="flex items-center">
|
||||
Select Issue <span className="text-red-500 ml-1">*</span>
|
||||
<span className="text-xs text-muted-foreground ml-2">(Required)</span>
|
||||
</Label>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Title</Label>
|
||||
<Input id="title" placeholder="Brief title for your perspective" required />
|
||||
{selectedIssueDetails ? (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between p-3 border rounded-md">
|
||||
<div>
|
||||
<p className="font-medium">{selectedIssueDetails.title}</p>
|
||||
<Badge variant="outline" className="mt-1">{selectedIssueDetails.category}</Badge>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedIssue(null)}
|
||||
>
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Your perspective will be linked to this issue
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="perspective">Your Perspective</Label>
|
||||
<Textarea
|
||||
id="perspective"
|
||||
placeholder="Share your thoughts, ideas, or concerns about this issue..."
|
||||
className="min-h-[150px]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Select>
|
||||
<SelectTrigger id="category">
|
||||
<SelectValue placeholder="Select a category (optional)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="suggestion">Suggestion</SelectItem>
|
||||
<SelectItem value="concern">Concern</SelectItem>
|
||||
<SelectItem value="question">Question</SelectItem>
|
||||
<SelectItem value="feedback">Feedback</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full" disabled={!walletConnected || isSubmitting} onClick={handleSubmit}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Submitting...
|
||||
</>
|
||||
) : (
|
||||
"Submit Perspective"
|
||||
<div>
|
||||
<Select
|
||||
value={selectedIssue || ""}
|
||||
onValueChange={(value) => setSelectedIssue(value)}
|
||||
required
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose an issue to respond to" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mockIssues.map((issue) => (
|
||||
<SelectItem key={issue.id} value={issue.id}>
|
||||
{issue.title} ({issue.category})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="mt-2 flex justify-between items-center">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Can't find the issue you're looking for?
|
||||
</p>
|
||||
<Button type="button" variant="link" size="sm" asChild className="p-0 h-auto">
|
||||
<Link href="/issues/propose">Propose a new issue</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!selectedIssue && (
|
||||
<Alert className="mb-4">
|
||||
<AlertTitle>Note</AlertTitle>
|
||||
<AlertDescription>
|
||||
All perspectives must be linked to a specific issue. This helps organize discussions and generate meaningful insights.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Title</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Give your perspective a clear, concise title"
|
||||
className="w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="content">Your Perspective</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Share your thoughts in detail. Consider including specific examples or suggestions."
|
||||
className="min-h-[200px]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Attachments */}
|
||||
<div className="space-y-2">
|
||||
<Label>Attachments (Optional)</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button type="button" variant="outline" className="gap-2">
|
||||
<Upload className="h-4 w-4" />
|
||||
Upload Files
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Supported formats: PDF, DOC, DOCX, JPG, PNG (max 10MB)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
className="gap-2"
|
||||
disabled={isSubmitting || !selectedIssue}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Submitting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="h-4 w-4" />
|
||||
Submit Perspective
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Guidelines */}
|
||||
<Card className="mt-8">
|
||||
<CardHeader>
|
||||
<CardTitle>Submission Guidelines</CardTitle>
|
||||
<CardDescription>Tips for submitting an effective perspective</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="list-disc space-y-2 pl-4 text-muted-foreground">
|
||||
<li>Be specific and provide concrete examples</li>
|
||||
<li>Focus on constructive solutions rather than just problems</li>
|
||||
<li>Consider the impact on different stakeholders</li>
|
||||
<li>Support your perspective with data when possible</li>
|
||||
<li>Keep your tone professional and respectful</li>
|
||||
<li>Review your submission for clarity before posting</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
Twitter,
|
||||
Github,
|
||||
Linkedin,
|
||||
ClipboardList,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
Sidebar,
|
||||
@ -51,7 +52,10 @@ export function AppSidebar() {
|
||||
const { walletConnected, walletAddress, username, disconnectWallet } = useWalletStore()
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return pathname === path
|
||||
if (path === '/') {
|
||||
return pathname === path
|
||||
}
|
||||
return pathname?.startsWith(path)
|
||||
}
|
||||
|
||||
const mainNavItems = [
|
||||
@ -60,6 +64,11 @@ export function AppSidebar() {
|
||||
icon: Home,
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
title: "Issues",
|
||||
icon: ClipboardList,
|
||||
href: "/issues",
|
||||
},
|
||||
{
|
||||
title: "Submit Perspective",
|
||||
icon: MessageSquare,
|
||||
|
@ -1,64 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Wallet } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { useWalletStore } from "@/lib/wallet-store"
|
||||
import { WalletConnectionModal } from "@/components/wallet-connection-modal"
|
||||
|
||||
export function ConnectWalletButton() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { connectWallet } = useWalletStore()
|
||||
|
||||
const handleConnect = (walletType: string) => {
|
||||
// In a real implementation, this would connect to MetaMask or other wallet
|
||||
// Generate a mock address based on wallet type for demo purposes
|
||||
const mockAddresses = {
|
||||
metamask: "0x1a2...3b4c",
|
||||
privado: "0x4d5...6e7f",
|
||||
}
|
||||
|
||||
connectWallet(mockAddresses[walletType as keyof typeof mockAddresses])
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-full" variant="outline">
|
||||
<Wallet className="mr-2 h-4 w-4" />
|
||||
Connect Wallet
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connect your wallet</DialogTitle>
|
||||
<DialogDescription>Connect your wallet to participate in VoxPop and have your voice heard.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<Button onClick={() => handleConnect("metamask")} className="w-full">
|
||||
<Wallet className="mr-2 h-4 w-4" />
|
||||
Connect with MetaMask
|
||||
</Button>
|
||||
<Button onClick={() => handleConnect("privado")} variant="outline" className="w-full">
|
||||
Connect with Privado ID
|
||||
</Button>
|
||||
</div>
|
||||
<DialogFooter className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button variant="secondary" onClick={() => setOpen(false)} className="w-full sm:w-auto">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
return <WalletConnectionModal />
|
||||
}
|
||||
|
||||
|
245
ui/prototype/components/wallet-connection-modal.tsx
Normal file
245
ui/prototype/components/wallet-connection-modal.tsx
Normal file
@ -0,0 +1,245 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Wallet, HelpCircle, Check, AlertTriangle, Loader2 } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { useWalletStore } from "@/lib/wallet-store"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const walletOptions = [
|
||||
{
|
||||
id: "metamask",
|
||||
name: "MetaMask",
|
||||
icon: "/images/wallets/metamask.svg",
|
||||
description: "Connect to your MetaMask wallet",
|
||||
popular: true
|
||||
},
|
||||
{
|
||||
id: "walletconnect",
|
||||
name: "WalletConnect",
|
||||
icon: "/images/wallets/walletconnect.svg",
|
||||
description: "Connect using WalletConnect",
|
||||
popular: true
|
||||
},
|
||||
{
|
||||
id: "coinbase",
|
||||
name: "Coinbase Wallet",
|
||||
icon: "/images/wallets/coinbase.svg",
|
||||
description: "Connect to your Coinbase wallet",
|
||||
popular: false
|
||||
},
|
||||
{
|
||||
id: "privadoid",
|
||||
name: "Privado ID",
|
||||
icon: "/images/wallets/privado.svg",
|
||||
description: "Connect with zero-knowledge verification",
|
||||
popular: true
|
||||
}
|
||||
]
|
||||
|
||||
export function WalletConnectionModal() {
|
||||
const { walletConnected, connectWallet } = useWalletStore()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [selectedWallet, setSelectedWallet] = useState<string | null>(null)
|
||||
const [connectionState, setConnectionState] = useState<"idle" | "connecting" | "success" | "error">("idle")
|
||||
const [errorMessage, setErrorMessage] = useState("")
|
||||
|
||||
const handleWalletSelect = (walletId: string) => {
|
||||
setSelectedWallet(walletId)
|
||||
}
|
||||
|
||||
const handleConnect = async () => {
|
||||
if (!selectedWallet) return
|
||||
|
||||
setConnectionState("connecting")
|
||||
setErrorMessage("")
|
||||
|
||||
try {
|
||||
// This would connect to the actual wallet in production
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)) // Simulate connection delay
|
||||
|
||||
// Simulate success or failure (in production, would use actual wallet connection)
|
||||
const success = Math.random() > 0.2 // 80% success rate for demo
|
||||
|
||||
if (success) {
|
||||
connectWallet() // Update global wallet state
|
||||
setConnectionState("success")
|
||||
setTimeout(() => {
|
||||
setIsOpen(false)
|
||||
setConnectionState("idle")
|
||||
setSelectedWallet(null)
|
||||
}, 1500)
|
||||
} else {
|
||||
throw new Error("Could not connect to wallet. Please try again.")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Wallet connection error:", error)
|
||||
setConnectionState("error")
|
||||
setErrorMessage(error instanceof Error ? error.message : "An unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
const closeAndReset = () => {
|
||||
setIsOpen(false)
|
||||
setTimeout(() => {
|
||||
setConnectionState("idle")
|
||||
setSelectedWallet(null)
|
||||
setErrorMessage("")
|
||||
}, 300)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant={walletConnected ? "outline" : "default"}>
|
||||
<Wallet className="mr-2 h-4 w-4" />
|
||||
{walletConnected ? "Wallet Connected" : "Connect Wallet"}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connect your wallet</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select a wallet to connect and authenticate with the platform.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{connectionState === "success" ? (
|
||||
<div className="flex flex-col items-center justify-center py-6">
|
||||
<div className="rounded-full bg-green-100 p-3 mb-4">
|
||||
<Check className="h-8 w-8 text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold">Connected Successfully</h3>
|
||||
<p className="text-muted-foreground mt-2">Your wallet is now connected</p>
|
||||
</div>
|
||||
) : connectionState === "error" ? (
|
||||
<div className="flex flex-col items-center justify-center py-6">
|
||||
<div className="rounded-full bg-red-100 p-3 mb-4">
|
||||
<AlertTriangle className="h-8 w-8 text-red-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold">Connection Failed</h3>
|
||||
<p className="text-muted-foreground mt-2">{errorMessage}</p>
|
||||
|
||||
<Button variant="default" onClick={() => setConnectionState("idle")} className="mt-4">
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Popular Wallets</h3>
|
||||
<div className="space-y-2">
|
||||
{walletOptions.filter(w => w.popular).map((wallet) => (
|
||||
<div
|
||||
key={wallet.id}
|
||||
className={cn(
|
||||
"flex items-center space-x-4 rounded-md border p-4 cursor-pointer transition-colors",
|
||||
selectedWallet === wallet.id
|
||||
? "border-primary bg-primary/5"
|
||||
: "hover:bg-muted"
|
||||
)}
|
||||
onClick={() => handleWalletSelect(wallet.id)}
|
||||
>
|
||||
<div className="h-10 w-10 flex-shrink-0 overflow-hidden rounded-full border">
|
||||
<div className="h-full w-full flex items-center justify-center bg-muted">
|
||||
<img
|
||||
src={wallet.icon}
|
||||
alt={wallet.name}
|
||||
className="h-6 w-6"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='6' width='20' height='12' rx='2'/%3E%3Cpath d='M22 10H18a2 2 0 0 0-2 2v0a2 2 0 0 0 2 2h4'/%3E%3C/svg%3E";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="font-medium leading-none">{wallet.name}</p>
|
||||
<p className="text-sm text-muted-foreground">{wallet.description}</p>
|
||||
</div>
|
||||
{selectedWallet === wallet.id && <Check className="h-5 w-5 text-primary" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Other Wallets</h3>
|
||||
<div className="space-y-2">
|
||||
{walletOptions.filter(w => !w.popular).map((wallet) => (
|
||||
<div
|
||||
key={wallet.id}
|
||||
className={cn(
|
||||
"flex items-center space-x-4 rounded-md border p-4 cursor-pointer transition-colors",
|
||||
selectedWallet === wallet.id
|
||||
? "border-primary bg-primary/5"
|
||||
: "hover:bg-muted"
|
||||
)}
|
||||
onClick={() => handleWalletSelect(wallet.id)}
|
||||
>
|
||||
<div className="h-10 w-10 flex-shrink-0 overflow-hidden rounded-full border">
|
||||
<div className="h-full w-full flex items-center justify-center bg-muted">
|
||||
<img
|
||||
src={wallet.icon}
|
||||
alt={wallet.name}
|
||||
className="h-6 w-6"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='6' width='20' height='12' rx='2'/%3E%3Cpath d='M22 10H18a2 2 0 0 0-2 2v0a2 2 0 0 0 2 2h4'/%3E%3C/svg%3E";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="font-medium leading-none">{wallet.name}</p>
|
||||
<p className="text-sm text-muted-foreground">{wallet.description}</p>
|
||||
</div>
|
||||
{selectedWallet === wallet.id && <Check className="h-5 w-5 text-primary" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex flex-col sm:flex-row gap-2">
|
||||
<Button variant="outline" onClick={closeAndReset}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConnect}
|
||||
disabled={!selectedWallet || connectionState === "connecting"}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{connectionState === "connecting" ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Connecting...
|
||||
</>
|
||||
) : (
|
||||
"Connect"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-center text-muted-foreground mt-2">
|
||||
<Button variant="link" size="sm" className="h-auto p-0">
|
||||
<HelpCircle className="h-3 w-3 mr-1" />
|
||||
What is a wallet?
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
4
ui/prototype/public/images/wallets/coinbase.svg
Normal file
4
ui/prototype/public/images/wallets/coinbase.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#2872FA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="9" fill="#2872FA" stroke="none"/>
|
||||
<circle cx="12" cy="12" r="5" fill="white" stroke="none"/>
|
||||
</svg>
|
After Width: | Height: | Size: 305 B |
5
ui/prototype/public/images/wallets/metamask.svg
Normal file
5
ui/prototype/public/images/wallets/metamask.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#E2761B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6 2h12l-2 7 3 1H5l3-1z" fill="#E2761B" stroke="#E2761B"/>
|
||||
<path d="M5 10v7c0 .6.4 1 1 1h4v3h4v-3h4c.6 0 1-.4 1-1v-7" stroke="#E2761B"/>
|
||||
<line x1="9" y1="17" x2="15" y2="17" stroke="#E2761B"/>
|
||||
</svg>
|
After Width: | Height: | Size: 371 B |
5
ui/prototype/public/images/wallets/privado.svg
Normal file
5
ui/prototype/public/images/wallets/privado.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#6D28D9" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" fill="#6D28D9" stroke="none"/>
|
||||
<path d="M12 8v6M9 10h6" stroke="white" stroke-width="2" />
|
||||
<path d="M8 16h8" stroke="white" stroke-width="1.5" />
|
||||
</svg>
|
After Width: | Height: | Size: 346 B |
4
ui/prototype/public/images/wallets/walletconnect.svg
Normal file
4
ui/prototype/public/images/wallets/walletconnect.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#3B99FC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" fill="#3B99FC" stroke="none"/>
|
||||
<path d="M8 11.5c2.5-2.5 5.5-2.5 8 0M9.5 13c1.5-1.5 3.5-1.5 5 0" stroke="white" stroke-width="1.5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 330 B |
Loading…
x
Reference in New Issue
Block a user