ui prototyping

This commit is contained in:
Justin Carper 2025-03-24 16:25:03 -04:00
parent 227740a13f
commit 66f170a21d
23 changed files with 2691 additions and 224 deletions

View File

@ -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.

View File

@ -41,7 +41,7 @@ The systems 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 @@ Well 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.

View File

@ -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

View File

@ -8,13 +8,13 @@
- **Why**: While youre 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. Pythons 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 colleagues Elm expertise, makes this a strong frontend combo. Elms 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 colleagues Next.js expertise, makes this a strong frontend combo. Typescripts 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 colleagues expertise.
### **4. Frontend with Next.js**
- **What**: Build a secure, user-friendly interface in Next.js, leveraging your colleagues 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 Gos ecosystem is less mature, using languages youve 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. Youll build a solid system thats 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. Youll build a solid system thats secure, efficient, and extensible, with room to grow into more advanced features like blockchain integration.

View File

@ -3,7 +3,7 @@
Your commitment to full decentralization is a brilliant shift—it aligns with blockchains 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. Heres 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**: Youll 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
Heres how decentralization reshapes your tech stack:
- **Frontend**:
- **Language**: Elm remains, but youll add wallet integration (e.g., MetaMask) and IPFS gateway logic.
- **Language**: Next.js remains, but youll 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, heres 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 backends role is minimized—smart contracts handle most logic, and other tasks are offloaded to decentralized services or user devices.

View File

@ -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 @@ Heres 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 @@ Heres 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 @@ Heres 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 |

View File

@ -77,7 +77,7 @@ Discourses 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.

View File

@ -0,0 +1,3 @@
export default function Loading() {
return <div className="container mx-auto p-4">Loading contribution details...</div>
}

View 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>
)
}

View File

@ -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)

View 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>
)
}

View 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>
)
}

View 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>
)
}

View File

@ -0,0 +1,3 @@
export default function Loading() {
return <div className="container mx-auto p-4">Loading resolution details...</div>
}

View 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>
)
}

View File

@ -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>
)

View File

@ -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,

View File

@ -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 />
}

View 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>
)
}

View 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

View 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

View 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

View 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