Compare commits

...

3 Commits
master ... poc

Author SHA1 Message Date
Justin Carper
66f170a21d ui prototyping 2025-03-24 16:25:03 -04:00
Justin Carper
227740a13f ui prototyping 2025-03-19 09:40:57 -04:00
Justin Carper
2ac5bd1b89 tiny poc 2025-03-03 02:00:12 -05:00
124 changed files with 17117 additions and 18 deletions

6
.gitignore vendored

@ -1 +1,5 @@
.idea
.idea
.env
vendor
.cursor/rules
.DS_Store

@ -57,7 +57,7 @@
## System Architecture Overview
- **Frontend**: A user-friendly interface (e.g., built with **Elm**) for submitting feedback, viewing legislation, and editing collaboratively.
- **Frontend**: A user-friendly interface (e.g., built with **Next.js**) for submitting feedback, viewing legislation, and editing collaboratively.
- **Backend**: Core logic (e.g., in **Go**) to process feedback, integrate AI, and interact with the blockchain.
- **Blockchain**: **Hyperledger Fabric** for storing data and managing smart contracts.
- **AI Services**: **Python**-based NLP and text generation models.

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

@ -51,9 +51,9 @@ Your system can be broken down into six core services, each with distinct respon
- Provide a user-friendly interface for feedback submission, editing, and tracking.
- Handle client-side cryptographic operations (e.g., ZKP generation).
- **Languages**:
- **Elm**: For the core UI logic (secure, functional, and maintainable).
- **Next.js**: For the core UI logic (secure, functional, and maintainable).
- **JavaScript**: For cryptographic tasks (e.g., ZKP generation via `snarkjs`).
- **Why Elm and JavaScript?**: Elm ensures a robust, type-safe UI, while JavaScript handles browser-based cryptography.
- **Why Next.js and JavaScript?**: Next.js ensures a robust, type-safe UI, while JavaScript handles browser-based cryptography.
- **Integration**:
- Communicates with backend services via gRPC-Web or a proxy.
- Uses WebSockets or polling for real-time updates (e.g., live legislative tracking).
@ -81,7 +81,7 @@ legislative-platform/
├── ai-service/ # Python: AI analysis and text generation
├── zkp-service/ # Go: ZKP verification
├── collaboration-service/ # Go: Git-like collaboration logic
├── frontend-service/ # Elm + JS: User interface and client-side crypto
├── frontend-service/ # Next.js + JS: User interface and client-side crypto
├── integration-service/ # Go: External system integration
├── shared/ # Common libraries, protos, etc.
├── docs/ # Documentation

@ -8,13 +8,13 @@
- **Why**: While 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.

@ -0,0 +1,167 @@
### Embracing Full Decentralization: Impact on System Design
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., 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.
#### 2. **Decentralized Identity and Access**
- **Change**: Fully integrate **Privado ID** for self-sovereign identity (SSI), replacing any centralized login systems.
- **Impact**: Users control their identities with zero-knowledge proofs, enhancing privacy and removing third-party dependencies.
- **Tech**: Add Privado IDs ZKP verification to your smart contracts.
- **Comfort Zone Shift**: Requires learning ZKP integration, but Privado IDs documentation and SDKs make it approachable.
#### 3. **Decentralized Governance**
- **Change**: Implement a **DAO** for system decisions (e.g., compiling Resolutions), using community votes instead of centralized control.
- **Impact**: No single entity can censor or control the system, ensuring its survival.
- **Tech**: Use **Aragon** or **Snapshot** for governance, integrated with your contracts.
- **Comfort Zone Shift**: Youll need to set up governance contracts and possibly a token, but templates simplify this.
#### 4. **Multi-Chain Deployment**
- **Change**: Deploy your smart contracts on multiple blockchains (e.g., Ethereum, Polygon, Optimism).
- **Impact**: If one chain faces restrictions, the system persists on others.
- **Tech**: Write chain-agnostic Solidity code and use **Hardhat** for multi-chain deployment.
- **Comfort Zone Shift**: Requires testing across networks, but EVM compatibility streamlines the process.
#### 5. **Decentralized Data Storage**
- **Change**: Store all off-chain data on **IPFS**, with **Filecoin** or **Arweave** for persistence.
- **Impact**: Data remains accessible without centralized servers, resisting censorship.
- **Tech**: Integrate IPFS uploads into your app (e.g., via `ipfs-http-client`).
- **Comfort Zone Shift**: Youll manage decentralized storage, but libraries make it manageable.
#### 6. **Peer-to-Peer Communication**
- **Change**: Use **libp2p** or **Whisper** for user interactions, avoiding centralized servers.
- **Impact**: Users communicate directly, enhancing decentralization.
- **Tech**: Add a P2P library to your frontend or backend.
- **Comfort Zone Shift**: This is more advanced, but you can start small and scale up.
#### 7. **Open-Source Everything**
- **Change**: Release all code on **GitHub** or **GitLab** under an open-source license.
- **Impact**: The community can fork and redeploy if needed, ensuring longevity.
- **Tech**: Maintain public repos with clear documentation.
- **Comfort Zone Shift**: Involves public collaboration, but it builds trust and resilience.
#### 8. **Decentralized DNS**
- **Change**: Use **Ethereum Name Service (ENS)** or **Unstoppable Domains** for your apps domain (e.g., `yourapp.eth`).
- **Impact**: Avoids traditional DNS censorship.
- **Tech**: Register a domain and link it to your IPFS-hosted frontend.
- **Comfort Zone Shift**: Requires managing blockchain domains, but its a simple process.
---
### Changes to Languages and Architecture
Heres how decentralization reshapes your tech stack:
- **Frontend**:
- **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.
- **Architecture**: Replaced by microservices handling IPFS, blockchain interactions, and P2P communication.
- **Smart Contracts**:
- **Language**: Solidity, enhanced with governance and multi-chain features.
- **Architecture**: Expanded to include DAO voting, access controls, and cross-chain compatibility.
- **Data Storage**:
- **Technology**: IPFS with Filecoin for long-term storage.
- **Architecture**: Fully decentralized, replacing any centralized databases.
- **Identity**:
- **Technology**: Privado ID for SSI.
- **Architecture**: ZKP-based, removing centralized providers.
- **Governance**:
- **Technology**: Aragon or Snapshot.
- **Architecture**: DAO-based, integrated into smart contracts.
---
### Summary
- **Data Authenticity**: Each method uses blockchain immutability, event logs, access controls, and cryptographic hashes to ensure only legitimate data is valid. Mocked data lacks the on-chain footprint to deceive the system.
- **Full Decentralization**: Shifts your system to IPFS-hosted frontends, Privado ID for identity, DAO governance, multi-chain deployment, and decentralized storage and communication. It requires learning new tools (IPFS, DAOs, ZKPs), but the result is a boundless, uncensorable platform.
---
To help you understand where the backend is deployed and what responsibilities it retains in a fully decentralized system, Ill break this down clearly and concisely. In a decentralized setup, the traditional concept of a backend—typically hosted on centralized servers—evolves to align with principles like censorship resistance, resilience, and the elimination of single points of failure. Lets dive into the details.
---
### Where Is the Backend Deployed?
In a fully decentralized system, the backend isnt hosted on a single server or cloud provider like AWS. Instead, its distributed across decentralized infrastructure to ensure no central authority controls it. Heres how and where its deployed:
1. **Smart Contracts on a Blockchain**
- **Location**: Deployed on a blockchain network, such as Ethereum or Polygon PoS.
- **Details**: Smart contracts are self-executing programs that live on the blockchain. Theyre accessible to anyone on the network and run on its nodes, making them inherently decentralized.
- **Why**: This eliminates reliance on a central server, ensuring the system remains operational as long as the blockchain exists.
2. **Decentralized Services for Off-Chain Tasks**
- **Location**: Hosted on decentralized platforms like:
- **IPFS (InterPlanetary File System)**: For static content or lightweight services.
- **Akash Network** or **Golem**: For running compute-intensive backend logic.
- **Fleeks Edge Functions**: For serverless, event-driven tasks.
- **Details**: These platforms distribute tasks across multiple nodes worldwide, avoiding centralized hosting.
- **Why**: They provide scalability and resilience for tasks that cant run directly on a blockchain.
3. **Peer-to-Peer (P2P) Networks**
- **Location**: Runs on user devices or decentralized nodes using protocols like **libp2p** or **Whisper**.
- **Details**: P2P networks enable direct communication between users without a central server.
- **Why**: This ensures real-time features (e.g., chat or notifications) remain decentralized and censorship-resistant.
In short, the backend is deployed across a blockchain for core logic, decentralized compute platforms for additional processing, and P2P networks for communication—no traditional servers required.
---
### What Responsibilities Does the Backend Retain?
Even in a decentralized system, certain backend-like responsibilities persist, but theyre handled by decentralized components rather than a central server. Heres what the backend (or its equivalent) still does:
1. **Data Processing and Validation**
- **What**: Ensures user inputs meet requirements before being stored or processed (e.g., checking the format of a submission).
- **How**: Handled by smart contracts or decentralized services to reduce blockchain costs and maintain quality.
- **Example**: Verifying that a text submission meets length criteria before recording it on-chain.
2. **Off-Chain Computation**
- **What**: Performs heavy computations that blockchains cant handle efficiently (e.g., AI analysis).
- **How**: Runs on decentralized compute platforms, with results submitted to the blockchain via oracles.
- **Example**: An AI model analyzes data stored on IPFS and submits the output to a smart contract.
3. **API Gateway for Frontend**
- **What**: Acts as an intermediary between the user interface and decentralized data sources (e.g., blockchain or IPFS).
- **How**: Provided by decentralized APIs hosted on IPFS or through direct blockchain queries (e.g., using The Graph).
- **Example**: Fetching data from IPFS to display in the app.
4. **Event Listening and Triggers**
- **What**: Monitors blockchain events and initiates actions based on them.
- **How**: Uses decentralized listeners or oracles to detect changes and trigger responses.
- **Example**: Detecting a new submission on the blockchain and starting an analysis process.
5. **User Authentication and Authorization**
- **What**: Manages identity and access in a decentralized way.
- **How**: Integrates with self-sovereign identity solutions (e.g., Privado ID) using zero-knowledge proofs.
- **Example**: Verifying a users eligibility to participate without a central login server.
---
### How This Differs from a Traditional Backend
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) |
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.
---
### Summary
- **Deployment**: The backend lives on a blockchain (smart contracts), decentralized compute platforms (e.g., Akash), and P2P networks (e.g., libp2p)—not on centralized servers.
- **Responsibilities**: It retains tasks like data validation, off-chain computation, frontend support, event handling, and decentralized authentication, all executed via distributed systems.

@ -0,0 +1,42 @@
## The Data Lifecycle
1. **Raw Submission**: Users submit their individual opinions or viewpoints on an issue, stored off-chain on IPFS.
2. **Processing**: These submissions are analyzed and refined—possibly by AI—to merge similar ideas, remove duplicates, and weight them by public opinion.
3. **Refined Output**: The processed data yields key themes or popular ideas derived from the submissions.
4. **Community Validation**: The community evaluates and votes on these outputs to determine which ones best represent the collective view.
5. **Accepted Input**: Validated outputs are accepted as meaningful contributions to the final result.
6. **Final Result**: The contributions are compiled into an actionable outcome that informs legislation or decision-making.
---
## Glossary of Terminology
- **Perspective**
*Definition*: The raw, unprocessed submission of a users opinion or viewpoint on a specific issue or piece of legislation, stored off-chain (on IPFS).
- **Synthesis**
*Definition*: The analytical process, often AI-driven, that refines and combines multiple perspectives into coherent insights. This involves merging similar ideas, removing duplicates, and weighting perspectives based on public opinion or voting.
- **Insight**
*Definition*: A refined and aggregated summary derived from one or more perspectives, representing common themes, popular ideas, or key takeaways that emerge from the synthesis process.
- **Consensus**
*Definition*: The communitys collective evaluation, voting, or endorsement of specific insights, determining which ones are most valuable, accurate, or representative of public opinion.
- **Contribution**
*Definition*: An insight that has been validated through consensus and accepted by the community as a meaningful input for the final decision-making process. Contributions are the building blocks of the projects output.
- **Resolution**
*Definition*: The conclusive result or output, derived from the contributions, that is used to inform legislation, policy, or other decision-making processes. This is the final, actionable outcome of the project.
---
## How It All Fits Together
1. **Perspective**: A user submits their raw opinion on a proposed law (e.g., "I think taxes should fund more public parks"), stored on IPFS.
2. **Synthesis**: AI analyzes hundreds of perspectives, merging similar ones (e.g., park funding ideas), removing duplicates, and weighting them by frequency of mentions, producing insights.
3. **Insight**: An insight emerges (e.g., "Most users support increased park funding as a tax priority").
4. **Consensus**: The community votes, agreeing that this insight reflects the publics view.
5. **Contribution**: The validated insight becomes a contribution, accepted as a key input.
6. **Resolution**: Contributions are compiled into a resolution (e.g., "Recommend tax allocation for parks"), sent to policymakers.

@ -0,0 +1,186 @@
## Revised Implementation Plan (Updated for Polygon PoS)
### 1. Blockchain Network Selection
- **Network**: **Polygon PoS**
- **Why**:
- **Low Costs**: Gas fees are minimal ($0.001$0.01 per transaction), perfect for a grassroots project.
- **High Throughput**: Handles up to 7,000 TPS, supporting early growth.
- **EVM Compatibility**: Works seamlessly with Ethereum tools like Solidity, Remix, and Hardhat.
- **Ecosystem Support**: Offers tools like The Graph for indexing and a robust developer community.
- **Future Outlook**: Keep an eye on **Polygon zkEVM** (in beta) for potential upgrades in privacy and scalability.
### 2. Identity Verification
- **Solution**: **Privado ID**
- **Why**: Provides privacy-preserving identity checks using zero-knowledge proofs (ZKPs).
- **Implementation**:
- Use Privado ID for secure, anonymous verification.
- Add optional simpler logins (e.g., email) to improve accessibility.
### 3. Data Storage and Indexing
- **Storage**:
- **On-Chain**: Store minimal data on Polygon PoS:
- Feedback hashes.
- Identifiers (e.g., feedback ID).
- ZKP proofs.
- **Off-Chain**: Use **IPFS** for full feedback data, pinned with **Pinata** for reliability.
- **Indexing**: Deploy a subgraph on **The Graph** to query on-chain events efficiently.
### 4. Feedback Analysis
- **Approach**:
- Run **off-chain AI analysis** (e.g., Hugging Face or spaCy) on feedback stored in IPFS.
- Combine with **community moderation** for accuracy.
- **Output**: Generate summaries or visualizations for users.
### 5. System Architecture
- **PoC**: **Monolithic** keeps it simple for initial development.
- **Production**: **Microservices** scales better as usage grows.
- **Components**: Identity, feedback submission, analysis, and indexing.
### 6. User Experience (UX)
- **Focus**: Intuitive and welcoming UX to drive adoption.
- **Tech**: Build the frontend with **Next.js** for a reliable, clean interface.
- **Goals**: Easy onboarding and clear feedback submission process.
### 7. Development Phases
- **Phase 1: Proof of Concept (PoC)**
- **Network**: **Polygon Amoy**
- **Steps**:
1. Deploy a Solidity contract for structured feedback (details below).
2. Integrate Privado ID and simpler logins.
3. Set up IPFS with Pinata.
4. Deploy a subgraph on The Graph.
5. Build an Next.js frontend.
6. Add off-chain AI and moderation.
7. Test end-to-end.
- **Phase 2: Production**
- Migrate to **Polygon mainnet**.
- Switch to microservices.
- Scale infrastructure.
### 8. Cost and Funding
- **Costs**:
- **PoC**: Free on Amoy; minimal off-chain costs (e.g., Pinata free tier).
- **Production**: ~$1,000 for 100,000 transactions on mainnet; Pinata at $20/month.
- **Funding Ideas**:
- Freemium model (basic free, premium paid).
- Charge for API access or explore a token system.
### 9. Future-Proofing
- **Scalability**: Consider **Polygon zkEVM** if needs outgrow PoS.
- **Privacy**: Leverage Privado IDs cross-chain features.
---
Heres a detailed breakdown of the full suite of dependencies, components, services, and their responsibilities based on the revised implementation plan for your project. This outline ties each part to the desired features while specifying the technologies and languages leveraged. Its designed to give you a clear, comprehensive view of how everything integrates.
---
## 1. Blockchain Network
- **Technology**: Polygon PoS (Polygon Amoy for testing)
- **Language**: Solidity (for smart contracts)
- **Responsibilities**:
- Stores minimal on-chain data, such as feedback hashes, identifiers, and zero-knowledge proof (ZKP) verifications, to keep costs low.
- Ensures transparency and verifiability of feedback submissions for trust and auditability.
- Executes smart contracts to handle feedback storage and ZKP verification logic.
---
## 2. Identity Verification
- **Technology**: Privado ID
- **Language**: Solidity (on-chain verification), JavaScript/TypeScript (off-chain proof generation)
- **Responsibilities**:
- Provides privacy-preserving identity verification using ZKPs, allowing users to prove eligibility (e.g., age or membership) without exposing sensitive details.
- Integrates off-chain proof generation with on-chain verification via smart contracts.
- Enhances user privacy and simplifies secure onboarding.
---
## 3. Data Storage
- **Technology**: IPFS (InterPlanetary File System) with Pinata for pinning
- **Language**: JavaScript/TypeScript (for IPFS interactions)
- **Responsibilities**:
- Stores full feedback data off-chain to minimize blockchain transaction costs.
- Ensures data reliability and accessibility by pinning files with Pinata.
- Links off-chain feedback to on-chain records using cryptographic hashes for integrity and reference.
---
## 4. Indexing
- **Technology**: The Graph
- **Language**: GraphQL (for querying), AssemblyScript (for subgraph development)
- **Responsibilities**:
- Indexes on-chain events (e.g., feedback submissions) to enable fast and efficient data retrieval.
- Creates a subgraph for seamless access to blockchain data by the frontend and backend.
- Supports complex queries to power analytics and user-facing features.
---
## 5. Feedback Analysis
- **Technology**: Hugging Face Transformers or spaCy
- **Language**: Python (for AI model integration)
- **Responsibilities**:
- Analyzes feedback data off-chain, performing tasks like sentiment analysis and categorization.
- Generates insights and summaries to inform users and moderators about trends or key points.
- Integrates with community moderation workflows to ensure accuracy and relevance.
---
## 6. System Architecture
- **Technology**: Microservices Architecture (production), Monolithic Architecture (Proof of Concept)
- **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.
- **AI Service**: Processes feedback analysis off-chain and delivers results.
- **Indexing Service**: Maintains and queries the subgraph for real-time data access.
---
## 7. User Experience (UX)
- **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.
- Presents analysis results and summaries in a clear, accessible format.
---
## 8. Development Phases
- **Technology**: Hardhat (smart contract development), Pinata (IPFS pinning)
- **Language**: JavaScript/TypeScript (deployment scripts)
- **Responsibilities**:
- **Proof of Concept (PoC) Phase**: Builds and tests the system on Polygon Amoy using a monolithic architecture.
- **Production Phase**: Transitions to Polygon mainnet with a scalable microservices architecture.
---
## 9. Cost and Funding
- **Technology**: Polygon PoS (low-cost transactions), Pinata (IPFS storage)
- **Language**: N/A
- **Responsibilities**:
- Minimizes expenses by storing minimal data on-chain and leveraging free-tier services where possible.
- Plans for sustainable funding through models like freemium access or API charges.
---
## 10. Future Considerations
- **Technology**: Polygon zkEVM (potential scalability upgrade)
- **Language**: Solidity (for zkEVM compatibility)
- **Responsibilities**:
- Evaluates Polygon zkEVM for future scalability and performance enhancements.
- Leverages Privado IDs cross-chain capabilities to maintain flexibility as the ecosystem evolves.
---
## Summary of Dependencies and Technologies
| **Component** | **Technology** | **Language** | **Key Feature** |
|-----------------------|------------------------------|-------------------------|----------------------------------|
| Blockchain | Polygon PoS | Solidity | Transparent feedback storage |
| Identity | Privado ID | Solidity, JS/TS | Privacy-preserving verification |
| 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), Next.js | Scalable system design |
| Frontend | Next.js | Next.js, CSS/HTML | User-friendly interface |
| Development Tools | Hardhat, Pinata | JS/TS | Streamlined development |

162
discovery/refined/INTRO.md Normal file

@ -0,0 +1,162 @@
Below is a highly detailed description of the system we are building, designed to be clear and comprehensive for anyone curious about the project—whether a human reader or an AI seeking context. It outlines the purpose, functionality, features, and technical architecture of the platform, reflecting its vision and potential impact on legislative processes.
---
## **Discourse: A Decentralized Platform for Crowdsourcing Public Opinion to Inform Legislation**
### **1. Introduction: Purpose and Vision**
Imagine a world where every citizen has a direct say in the laws that shape their lives. **Discourse** is a decentralized platform that makes this vision a reality by crowdsourcing public opinion to inform legislation. Built on blockchain technology, artificial intelligence (AI), and zero-knowledge proofs (ZKPs), Discourse empowers individuals to submit their opinions, collaborate on refining them, and influence policymakers—all while ensuring transparency, security, and resistance to censorship.
#### **Why Build Discourse?**
Traditional legislative systems often leave citizens feeling disconnected, with limited opportunities to influence outcomes. These processes can be opaque, dominated by special interests, and prone to voter apathy. Discourse addresses these challenges by:
- **Empowering Citizens**: Providing a direct, tamper-proof channel for public input into law-making.
- **Ensuring Transparency**: Recording every step on a blockchain for public scrutiny.
- **Harnessing Collective Wisdom**: Using AI and community voting to turn raw opinions into actionable policy recommendations.
- **Protecting Privacy**: Leveraging decentralized identity solutions to safeguard user data.
- **Resisting Control**: Operating on decentralized infrastructure to prevent censorship or shutdown.
The purpose of Discourse is to democratize governance, giving people a voice in shaping the policies that affect them while fostering trust through openness and accountability.
---
### **2. How It Works: The Data Lifecycle**
Discourse transforms individual opinions into polished policy recommendations through a structured, transparent process. Heres how it works, step by step:
#### **The Lifecycle Stages**
1. **Perspective Submission**
- **What**: Users submit their opinions, called **Perspectives**, on specific laws or issues (e.g., "I think taxes should fund more public parks").
- **How**: Perspectives are uploaded to the InterPlanetary File System (IPFS), a decentralized storage network. A cryptographic hash of each submission, along with the users address and an issue identifier, is recorded on the Polygon Proof-of-Stake (PoS) blockchain.
- **Example**: A citizen submits a Perspective via the platforms frontend. The system stores the text on IPFS and logs the hash on-chain, ensuring its verifiable and tamper-proof.
2. **Synthesis**
- **What**: AI analyzes Perspectives to identify trends, merge similar ideas, and produce **Insights**—concise summaries of public sentiment.
- **How**: Off-chain AI tools (e.g., natural language processing models from Hugging Face) process the raw data. An Insight might read, "Most users support increased park funding over other tax priorities."
- **Example**: After analyzing 100 Perspectives, the AI finds 70% favor park funding. This Insight is proposed and recorded on-chain with links to the original data.
3. **Consensus**
- **What**: The community votes to endorse or reject Insights, determining which ones reflect collective opinion.
- **How**: Users vote via the platforms smart contracts, with options for direct voting or delegating votes through liquid democracy. Votes are recorded on-chain.
- **Example**: An Insight about park funding receives 60% approval from 1,000 voters, surpassing the 50% threshold.
4. **Contribution**
- **What**: Insights that pass the voting threshold become **Contributions**, validated inputs for policymaking.
- **How**: The smart contract automatically marks an Insight as a Contribution once it meets the required support level.
- **Example**: The park funding Insight becomes a Contribution, logged on-chain as a community-endorsed idea.
5. **Resolution**
- **What**: Contributions are compiled into a **Resolution**, a final set of recommendations for policymakers.
- **How**: A Decentralized Autonomous Organization (DAO) or authorized moderators compile Contributions into a cohesive document, recorded on-chain.
- **Example**: A Resolution combining park funding and other Contributions is finalized and sent to local government officials.
#### **Analogy**
Think of this process like refining raw ore into precious metal: Perspectives are the raw material, Synthesis extracts the valuable essence, Consensus polishes it through community input, and Contributions are forged into a Resolution—a finished product ready for real-world use.
---
### **3. Technical Architecture**
Discourses architecture is designed to be decentralized, scalable, and resilient, integrating blockchain, decentralized storage, and off-chain computation.
#### **Core Components**
- **Blockchain Layer: Polygon PoS**
- **Why**: Low-cost transactions (cents per action), high throughput (up to 7,000 transactions per second), and compatibility with Ethereum Virtual Machine (EVM) tools.
- **Role**: Hosts smart contracts that manage the lifecycle—recording Perspectives, tracking votes, and finalizing Resolutions.
- **Decentralized Storage: IPFS with Pinata**
- **Why**: Cost-efficient, censorship-resistant storage for large datasets like Perspectives.
- **Role**: Stores full text submissions off-chain, linked to on-chain hashes for verification.
- **Data Indexing: The Graph**
- **Why**: Fast, decentralized querying of blockchain data.
- **Role**: Creates a subgraph to index events (e.g., all Perspectives on a specific issue), making data retrieval efficient.
- **Identity Verification: Privado ID**
- **Why**: Uses ZKPs to verify eligibility (e.g., citizenship) without exposing personal details.
- **Role**: Ensures only authorized users participate while protecting their privacy.
- **AI Analysis: Hugging Face Transformers or spaCy**
- **Why**: Robust tools for natural language processing and sentiment analysis.
- **Role**: Synthesizes Perspectives into Insights off-chain, feeding results back to the blockchain.
- **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.
- **Governance: DAO**
- **Why**: Enables community control over key decisions.
- **Role**: Uses tools like Aragon or Snapshot to manage voting and Resolution compilation.
#### **System Flow**
1. A user submits a Perspective via the IPFS-hosted frontend.
2. The frontend calls a smart contract on Polygon PoS to record the hash and metadata.
3. Off-chain AI analyzes Perspectives and submits Insights to the contract.
4. Users vote on Insights through the frontend, with votes logged on-chain.
5. The smart contract validates Insights as Contributions based on voting results.
6. The DAO compiles Contributions into a Resolution, finalized on the blockchain.
This modular design ensures the system is secure, scalable, and resistant to single points of failure.
---
### **4. Key Features**
Discourse stands out by blending advanced technologies into a cohesive, user-centric platform. Here are its defining features:
- **Decentralized Identity Verification**
- Privado ID uses ZKPs to confirm user eligibility (e.g., "Im a citizen") without revealing sensitive data, balancing access with privacy.
- **Liquid Democracy**
- Users can vote directly or delegate their votes to trusted peers, offering flexibility and amplifying participation.
- **AI-Powered Analysis**
- AI distills thousands of Perspectives into clear Insights, ensuring quality and reducing redundancy.
- **Community-Driven Governance**
- A DAO oversees critical decisions, from Insight validation to Resolution approval, keeping power in the hands of users.
- **Censorship Resistance**
- With IPFS hosting the frontend and Polygon PoS securing the backend, no single entity can shut down or alter the system.
- **Multi-Chain Potential**
- Built for EVM compatibility, Discourse can expand to other blockchains (e.g., Ethereum, Optimism) for added robustness.
- **Immutable Transparency**
- Every action—submissions, votes, and Resolutions—is permanently recorded on-chain, open to public audit.
---
### **5. Addressing Current Challenges**
Discourse tackles systemic issues in traditional legislative processes:
- **Opacity**: Unlike closed-door law-making, Discourses blockchain provides a transparent, verifiable record.
- **Limited Participation**: It replaces sporadic elections with continuous, direct input opportunities.
- **Special Interest Dominance**: Decentralization and community voting reduce the sway of lobbyists.
- **Voter Apathy**: By showing tangible impact (e.g., Resolutions influencing policy), it re-engages citizens.
- **Censorship Risks**: Its decentralized nature ensures voices cant be silenced by governments or corporations.
These solutions position Discourse as a transformative tool for participatory democracy.
---
### **6. Future Considerations**
Discourse is built to evolve. Potential upgrades include:
- **Scalability**: Transition to Polygon zkEVM for enhanced privacy and higher transaction capacity.
- **Expansion**: Deploy on additional blockchains to broaden reach and resilience.
- **AI Improvements**: Integrate advanced models for deeper sentiment analysis.
- **Governance Refinement**: Introduce token incentives or refine DAO processes based on user feedback.
- **Global Reach**: Localize the platform for multiple languages and jurisdictions.
These enhancements will keep Discourse adaptable to growing demand and technological advancements.
---
### **Glossary of Terms**
- **Perspective**: A users raw opinion or idea.
- **Synthesis**: AI-driven refinement of Perspectives into Insights.
- **Insight**: A summarized representation of public sentiment.
- **Consensus**: Community approval of Insights via voting.
- **Contribution**: A validated Insight ready for policymaking.
- **Resolution**: The final set of recommendations for lawmakers.
---
## **Conclusion**
Discourse is a pioneering platform that reimagines how laws are shaped, putting power back into the hands of citizens. By blending blockchains transparency, AIs intelligence, and decentralized governance, it offers a secure, inclusive, and resilient way to influence legislation. Whether youre a curious onlooker, a developer, or a policymaker, Discourse showcases a future where democracy is participatory, transparent, and truly representative—one Perspective at a time.

@ -0,0 +1,107 @@
## Enhancing the Smart Contract
Your process involves a clear progression: **Perspectives** (raw opinions stored on IPFS), **Synthesis** (AI-generated Insights), **Consensus** (community voting), **Contributions** (validated Insights), and **Resolutions** (final recommendations for policymakers). The smart contract should manage this flow, ensuring transparency, immutability, and community participation. Heres what it should do and the methods it needs:
### What the Smart Contract Should Do
- **Store Submissions**: Record Perspectives with their IPFS hashes, linked to specific issues, and track submitters (anonymously if needed).
- **Facilitate Insight Proposals**: Allow authorized entities (e.g., moderators or AI oracles) to propose Insights based on Perspectives.
- **Enable Voting**: Let the community vote on Insights to reach Consensus.
- **Validate Contributions**: Accept Insights as Contributions once they meet a voting threshold.
- **Compile Resolutions**: Aggregate Contributions into a final Resolution for policymakers.
- **Ensure Transparency**: Emit events for every step so actions are trackable off-chain.
- **Control Access**: Restrict certain actions (e.g., proposing Insights) to authorized roles while keeping the system open for participation.
### Additional Smart Contract Methods
Here are the key methods to implement this functionality:
1. **`submitPerspective`**
- **Purpose**: Lets users submit their raw opinions (Perspectives).
- **Inputs**: `ipfsHash` (string, the IPFS hash of the Perspective), `issueId` (string, identifies the proposed law or issue).
- **Action**: Stores the Perspective, links it to the submitters address, and emits an event.
- **Example**: A user submits "I think taxes should fund more public parks."
2. **`proposeInsight`**
- **Purpose**: Allows Insights to be proposed after AI synthesis of Perspectives.
- **Inputs**: `description` (string, the Insight text), `perspectiveIds` (array of Perspective IDs used to form the Insight).
- **Action**: Records the Insight, ties it to relevant Perspectives, and emits an event.
- **Example**: An AI proposes "Most users support increased park funding as a tax priority."
3. **`voteOnInsight`**
- **Purpose**: Enables community voting on Insights to reach Consensus.
- **Inputs**: `insightId` (uint, the Insights ID), `support` (boolean, true for yes, false for no).
- **Action**: Updates the vote count and emits an event; may include checks to prevent double-voting.
- **Example**: Users vote on the park funding Insight.
4. **`acceptContribution`**
- **Purpose**: Validates an Insight as a Contribution once Consensus is reached.
- **Inputs**: `insightId` (uint, the Insights ID).
- **Action**: Checks if the vote count meets a threshold (e.g., 50% of voters), marks it as accepted, and emits an event.
- **Example**: The park funding Insight becomes a Contribution.
5. **`compileResolution`**
- **Purpose**: Combines Contributions into a final Resolution.
- **Inputs**: `resolution` (string, the compiled text).
- **Action**: Records the Resolution and emits an event for off-chain use (e.g., sending to policymakers).
- **Example**: "Recommend tax allocation for parks" is compiled.
### Example Smart Contract Code
Heres a simplified version to illustrate:
```solidity
contract LegislationCrowdsourcing {
struct Perspective {
string ipfsHash; // IPFS hash of the raw opinion
string issueId; // Identifies the issue or law
address submitter; // Submitters address
}
struct Insight {
string description; // Insight text
uint256 perspectiveCount; // Number of Perspectives its based on
uint256 voteCount; // Number of supporting votes
bool accepted; // True if it becomes a Contribution
}
mapping(uint256 => Perspective) public perspectives;
mapping(uint256 => Insight) public insights;
uint256 public perspectiveCount;
uint256 public insightCount;
event PerspectiveSubmitted(uint256 id, string ipfsHash, string issueId, address submitter);
event InsightProposed(uint256 id, string description);
event VotedOnInsight(uint256 insightId, address voter, bool support);
event ContributionAccepted(uint256 insightId);
event ResolutionCompiled(string resolution);
function submitPerspective(string memory ipfsHash, string memory issueId) public {
perspectives[perspectiveCount] = Perspective(ipfsHash, issueId, msg.sender);
emit PerspectiveSubmitted(perspectiveCount, ipfsHash, issueId, msg.sender);
perspectiveCount++;
}
function proposeInsight(string memory description, uint256[] memory perspectiveIds) public {
insights[insightCount] = Insight(description, perspectiveIds.length, 0, false);
emit InsightProposed(insightCount, description);
insightCount++;
}
function voteOnInsight(uint256 insightId, bool support) public {
if (support) {
insights[insightId].voteCount++;
}
emit VotedOnInsight(insightId, msg.sender, support);
}
function acceptContribution(uint256 insightId) public {
uint256 threshold = 100; // Example threshold
if (insights[insightId].voteCount >= threshold) {
insights[insightId].accepted = true;
emit ContributionAccepted(insightId);
}
}
function compileResolution(string memory resolution) public {
emit ResolutionCompiled(resolution);
}
}
```

36
go.mod Normal file

@ -0,0 +1,36 @@
module 4vif5i.gitea.cloud/numenor-labs/discourse
go 1.23.0
toolchain go1.23.6
require github.com/hyperledger/fabric-contract-api-go v1.2.2
require (
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/gobuffalo/envy v1.10.2 // indirect
github.com/gobuffalo/packd v1.0.2 // indirect
github.com/gobuffalo/packr v1.30.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/hyperledger/fabric-chaincode-go v0.0.0-20230731094759-d626e9ab09b9 // indirect
github.com/hyperledger/fabric-protos-go v0.3.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

152
go.sum Normal file

@ -0,0 +1,152 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4=
github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8=
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw=
github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8=
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hyperledger/fabric-chaincode-go v0.0.0-20230731094759-d626e9ab09b9 h1:XV1mxAmExeWraP5AmBSB1v415jMCSFJ087dRUiI6f6o=
github.com/hyperledger/fabric-chaincode-go v0.0.0-20230731094759-d626e9ab09b9/go.mod h1:WEd2Rlyj47/8b0VvH/zYPKamLdU3hg7jWqV8XEBTLOk=
github.com/hyperledger/fabric-contract-api-go v1.2.2 h1:zun9/BmaIWFSSOkfQXikdepK0XDb7MkJfc/lb5j3ku8=
github.com/hyperledger/fabric-contract-api-go v1.2.2/go.mod h1:UnFLlRFn8GvXE7mXxWtU+bESM7fb5YzsKo1DA16vvaE=
github.com/hyperledger/fabric-protos-go v0.3.0 h1:MXxy44WTMENOh5TI8+PCK2x6pMj47Go2vFRKDHB2PZs=
github.com/hyperledger/fabric-protos-go v0.3.0/go.mod h1:WWnyWP40P2roPmmvxsUXSvVI/CF6vwY1K1UFidnKBys=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 h1:AB/lmRny7e2pLhFEYIbl5qkDAUt2h0ZRO4wGPhZf+ik=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -0,0 +1,264 @@
// Code generated - DO NOT EDIT.
// This file is a generated binding and any manual changes will be lost.
package feedback
import (
"errors"
"math/big"
"strings"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
)
// Reference imports to suppress errors if they are not otherwise used.
var (
_ = errors.New
_ = big.NewInt
_ = strings.NewReader
_ = ethereum.NotFound
_ = bind.Bind
_ = common.Big1
_ = types.BloomLookup
_ = event.NewSubscription
_ = abi.ConvertType
)
// FeedbackMetaData contains all meta data concerning the Feedback contract.
var FeedbackMetaData = &bind.MetaData{
ABI: "[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"feedbackList\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"index\",\"type\":\"uint256\"}],\"name\":\"getFeedback\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"feedback\",\"type\":\"string\"}],\"name\":\"submitFeedback\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]",
}
// FeedbackABI is the input ABI used to generate the binding from.
// Deprecated: Use FeedbackMetaData.ABI instead.
var FeedbackABI = FeedbackMetaData.ABI
// Feedback is an auto generated Go binding around an Ethereum contract.
type Feedback struct {
FeedbackCaller // Read-only binding to the contract
FeedbackTransactor // Write-only binding to the contract
FeedbackFilterer // Log filterer for contract events
}
// FeedbackCaller is an auto generated read-only Go binding around an Ethereum contract.
type FeedbackCaller struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// FeedbackTransactor is an auto generated write-only Go binding around an Ethereum contract.
type FeedbackTransactor struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// FeedbackFilterer is an auto generated log filtering Go binding around an Ethereum contract events.
type FeedbackFilterer struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// FeedbackSession is an auto generated Go binding around an Ethereum contract,
// with pre-set call and transact options.
type FeedbackSession struct {
Contract *Feedback // Generic contract binding to set the session for
CallOpts bind.CallOpts // Call options to use throughout this session
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
}
// FeedbackCallerSession is an auto generated read-only Go binding around an Ethereum contract,
// with pre-set call options.
type FeedbackCallerSession struct {
Contract *FeedbackCaller // Generic contract caller binding to set the session for
CallOpts bind.CallOpts // Call options to use throughout this session
}
// FeedbackTransactorSession is an auto generated write-only Go binding around an Ethereum contract,
// with pre-set transact options.
type FeedbackTransactorSession struct {
Contract *FeedbackTransactor // Generic contract transactor binding to set the session for
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
}
// FeedbackRaw is an auto generated low-level Go binding around an Ethereum contract.
type FeedbackRaw struct {
Contract *Feedback // Generic contract binding to access the raw methods on
}
// FeedbackCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract.
type FeedbackCallerRaw struct {
Contract *FeedbackCaller // Generic read-only contract binding to access the raw methods on
}
// FeedbackTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract.
type FeedbackTransactorRaw struct {
Contract *FeedbackTransactor // Generic write-only contract binding to access the raw methods on
}
// NewFeedback creates a new instance of Feedback, bound to a specific deployed contract.
func NewFeedback(address common.Address, backend bind.ContractBackend) (*Feedback, error) {
contract, err := bindFeedback(address, backend, backend, backend)
if err != nil {
return nil, err
}
return &Feedback{FeedbackCaller: FeedbackCaller{contract: contract}, FeedbackTransactor: FeedbackTransactor{contract: contract}, FeedbackFilterer: FeedbackFilterer{contract: contract}}, nil
}
// NewFeedbackCaller creates a new read-only instance of Feedback, bound to a specific deployed contract.
func NewFeedbackCaller(address common.Address, caller bind.ContractCaller) (*FeedbackCaller, error) {
contract, err := bindFeedback(address, caller, nil, nil)
if err != nil {
return nil, err
}
return &FeedbackCaller{contract: contract}, nil
}
// NewFeedbackTransactor creates a new write-only instance of Feedback, bound to a specific deployed contract.
func NewFeedbackTransactor(address common.Address, transactor bind.ContractTransactor) (*FeedbackTransactor, error) {
contract, err := bindFeedback(address, nil, transactor, nil)
if err != nil {
return nil, err
}
return &FeedbackTransactor{contract: contract}, nil
}
// NewFeedbackFilterer creates a new log filterer instance of Feedback, bound to a specific deployed contract.
func NewFeedbackFilterer(address common.Address, filterer bind.ContractFilterer) (*FeedbackFilterer, error) {
contract, err := bindFeedback(address, nil, nil, filterer)
if err != nil {
return nil, err
}
return &FeedbackFilterer{contract: contract}, nil
}
// bindFeedback binds a generic wrapper to an already deployed contract.
func bindFeedback(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) {
parsed, err := FeedbackMetaData.GetAbi()
if err != nil {
return nil, err
}
return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil
}
// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func (_Feedback *FeedbackRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error {
return _Feedback.Contract.FeedbackCaller.contract.Call(opts, result, method, params...)
}
// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (_Feedback *FeedbackRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
return _Feedback.Contract.FeedbackTransactor.contract.Transfer(opts)
}
// Transact invokes the (paid) contract method with params as input values.
func (_Feedback *FeedbackRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
return _Feedback.Contract.FeedbackTransactor.contract.Transact(opts, method, params...)
}
// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func (_Feedback *FeedbackCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error {
return _Feedback.Contract.contract.Call(opts, result, method, params...)
}
// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (_Feedback *FeedbackTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
return _Feedback.Contract.contract.Transfer(opts)
}
// Transact invokes the (paid) contract method with params as input values.
func (_Feedback *FeedbackTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
return _Feedback.Contract.contract.Transact(opts, method, params...)
}
// FeedbackList is a free data retrieval call binding the contract method 0x5d837247.
//
// Solidity: function feedbackList(uint256 ) view returns(string)
func (_Feedback *FeedbackCaller) FeedbackList(opts *bind.CallOpts, arg0 *big.Int) (string, error) {
var out []interface{}
err := _Feedback.contract.Call(opts, &out, "feedbackList", arg0)
if err != nil {
return *new(string), err
}
out0 := *abi.ConvertType(out[0], new(string)).(*string)
return out0, err
}
// FeedbackList is a free data retrieval call binding the contract method 0x5d837247.
//
// Solidity: function feedbackList(uint256 ) view returns(string)
func (_Feedback *FeedbackSession) FeedbackList(arg0 *big.Int) (string, error) {
return _Feedback.Contract.FeedbackList(&_Feedback.CallOpts, arg0)
}
// FeedbackList is a free data retrieval call binding the contract method 0x5d837247.
//
// Solidity: function feedbackList(uint256 ) view returns(string)
func (_Feedback *FeedbackCallerSession) FeedbackList(arg0 *big.Int) (string, error) {
return _Feedback.Contract.FeedbackList(&_Feedback.CallOpts, arg0)
}
// GetFeedback is a free data retrieval call binding the contract method 0x1106a382.
//
// Solidity: function getFeedback(uint256 index) view returns(string)
func (_Feedback *FeedbackCaller) GetFeedback(opts *bind.CallOpts, index *big.Int) (string, error) {
var out []interface{}
err := _Feedback.contract.Call(opts, &out, "getFeedback", index)
if err != nil {
return *new(string), err
}
out0 := *abi.ConvertType(out[0], new(string)).(*string)
return out0, err
}
// GetFeedback is a free data retrieval call binding the contract method 0x1106a382.
//
// Solidity: function getFeedback(uint256 index) view returns(string)
func (_Feedback *FeedbackSession) GetFeedback(index *big.Int) (string, error) {
return _Feedback.Contract.GetFeedback(&_Feedback.CallOpts, index)
}
// GetFeedback is a free data retrieval call binding the contract method 0x1106a382.
//
// Solidity: function getFeedback(uint256 index) view returns(string)
func (_Feedback *FeedbackCallerSession) GetFeedback(index *big.Int) (string, error) {
return _Feedback.Contract.GetFeedback(&_Feedback.CallOpts, index)
}
// SubmitFeedback is a paid mutator transaction binding the contract method 0xe341a623.
//
// Solidity: function submitFeedback(string feedback) returns()
func (_Feedback *FeedbackTransactor) SubmitFeedback(opts *bind.TransactOpts, feedback string) (*types.Transaction, error) {
return _Feedback.contract.Transact(opts, "submitFeedback", feedback)
}
// SubmitFeedback is a paid mutator transaction binding the contract method 0xe341a623.
//
// Solidity: function submitFeedback(string feedback) returns()
func (_Feedback *FeedbackSession) SubmitFeedback(feedback string) (*types.Transaction, error) {
return _Feedback.Contract.SubmitFeedback(&_Feedback.TransactOpts, feedback)
}
// SubmitFeedback is a paid mutator transaction binding the contract method 0xe341a623.
//
// Solidity: function submitFeedback(string feedback) returns()
func (_Feedback *FeedbackTransactorSession) SubmitFeedback(feedback string) (*types.Transaction, error) {
return _Feedback.Contract.SubmitFeedback(&_Feedback.TransactOpts, feedback)
}

60
poc/backend/go.mod Normal file

@ -0,0 +1,60 @@
module 4vif5i.gitea.cloud/numenor-labs/discourse/poc/backend
go 1.23.6
require (
github.com/ethereum/go-ethereum v1.15.4
github.com/gin-gonic/gin v1.10.0
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/bits-and-blooms/bitset v1.17.0 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/consensys/bavard v0.1.22 // indirect
github.com/consensys/gnark-crypto v0.14.0 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/supranational/blst v0.3.14 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)

261
poc/backend/go.sum Normal file

@ -0,0 +1,261 @@
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI=
github.com/bits-and-blooms/bitset v1.17.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA=
github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/consensys/bavard v0.1.22 h1:Uw2CGvbXSZWhqK59X0VG/zOjpTFuOMcPLStrp1ihI0A=
github.com/consensys/bavard v0.1.22/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs=
github.com/consensys/gnark-crypto v0.14.0 h1:DDBdl4HaBtdQsq/wfMwJvZNE80sHidrK3Nfrefatm0E=
github.com/consensys/gnark-crypto v0.14.0/go.mod h1:CU4UijNPsHawiVGNxe9co07FkzCeWHHrb1li/n1XoU0=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4=
github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
github.com/ethereum/go-ethereum v1.15.4 h1:a0P+AalZaosp97rfKoYXHYWzyK3+jXWZrciM9S7XFrI=
github.com/ethereum/go-ethereum v1.15.4/go.mod h1:1LG2LnMOx2yPRHR/S+xuipXH29vPr6BIH6GElD8N/fo=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4=
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y=
github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo=
github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=

97
poc/backend/main.go Normal file

@ -0,0 +1,97 @@
package main
import (
"fmt"
"log"
"math/big"
"net/http"
"os"
"4vif5i.gitea.cloud/numenor-labs/discourse/poc/backend/feedback"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/gin-gonic/gin"
)
var (
rpcURL = os.Getenv("RPC_URL")
contractAddress = os.Getenv("CONTRACT_ADDRESS")
privateKey = os.Getenv("PRIVATE_KEY")
)
func main() {
// Connect to Sepolia
client, err := ethclient.Dial(rpcURL)
if err != nil {
log.Fatalf("Failed to connect to Ethereum client: %v", err)
}
// Set up HTTP server
router := gin.Default()
router.OPTIONS(
"/submit-feedback", func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type")
c.Status(http.StatusOK)
},
)
router.POST(
"/submit-feedback", func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type")
var request struct {
Proof string `json:"proof"`
Feedback string `json:"feedback"`
}
if err := c.BindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// Mocked ZKP verification
if request.Proof != "valid_proof" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid proof"})
return
}
privateKeyECDSA, err := crypto.HexToECDSA(privateKey)
if err != nil {
fmt.Println("Failed to parse private key:", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse private key"})
return
}
// Create the transactor (chain ID 11155111 is for Sepolia testnet, adjust as needed)
auth, err := bind.NewKeyedTransactorWithChainID(privateKeyECDSA, big.NewInt(11155111))
if err != nil {
log.Fatalf("Failed to create transactor: %v", err)
}
// Instantiate the contract
contract, err := feedback.NewFeedback(common.HexToAddress(contractAddress), client)
if err != nil {
fmt.Println("Failed to instantiate contract:", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to instantiate contract"})
return
}
// Submit feedback to blockchain
tx, err := contract.SubmitFeedback(auth, request.Feedback)
if err != nil {
fmt.Println("Failed to submit feedback:", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to submit feedback"})
return
}
fmt.Println("Submitted feedback:", tx.Hash().Hex())
c.JSON(http.StatusOK, gin.H{"transaction": tx.Hash().Hex()})
},
)
log.Println("Server running on :3000")
router.Run(":3000")
}

53
poc/contract/Feedback.abi Normal file

@ -0,0 +1,53 @@
[
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "feedbackList",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "getFeedback",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "feedback",
"type": "string"
}
],
"name": "submitFeedback",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]

56
poc/contract/Feedback.sol Normal file

@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
struct FeedbackRecord {
string feedbackHash; // Hash of the full feedback data stored on IPFS
string issueId; // Unique identifier for the issue or legislation
bytes zkpProof; // ZKP proof for identity verification
uint256 timestamp; // Timestamp of submission
address submitter; // Address of the submitter (optional)
}
contract FeedbackStorage {
// Array to store all feedback records
FeedbackRecord[] public feedbackRecords;
// Event to emit when feedback is submitted (for off-chain indexing)
event FeedbackSubmitted(
uint256 indexed id,
string feedbackHash,
string issueId,
bytes zkpProof,
uint256 timestamp,
address submitter
);
// Function to submit feedback
function submitFeedback(
string memory feedbackHash,
string memory issueId,
bytes memory zkpProof
) public {
feedbackRecords.push(FeedbackRecord({
feedbackHash: feedbackHash,
issueId: issueId,
zkpProof: zkpProof,
timestamp: block.timestamp,
submitter: msg.sender
}));
// Emit event for tracking and indexing
emit FeedbackSubmitted(
feedbackRecords.length - 1,
feedbackHash,
issueId,
zkpProof,
block.timestamp,
msg.sender
);
}
// Function to get the total number of feedback records (optional utility)
function getFeedbackCount() public view returns (uint256) {
return feedbackRecords.length;
}
}

27
poc/frontend/elm.json Normal file

@ -0,0 +1,27 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3"
},
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.3"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

83
poc/frontend/src/Main.elm Normal file

@ -0,0 +1,83 @@
module Main exposing (..)
import Browser
import Html exposing (Html, button, div, input, text)
import Html.Events exposing (onClick, onInput)
import Http
import Json.Encode as Encode
-- Model
type alias Model =
{ proof : String
, feedback : String
, response : String
}
init : Model
init =
{ proof = ""
, feedback = ""
, response = ""
}
-- Messages
type Msg
= UpdateProof String
| UpdateFeedback String
| Submit
| GotResponse (Result Http.Error String)
-- Update
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UpdateProof proof ->
( { model | proof = proof }, Cmd.none )
UpdateFeedback feedback ->
( { model | feedback = feedback }, Cmd.none )
Submit ->
( model, submitFeedback model.proof model.feedback )
GotResponse result ->
case result of
Ok response ->
( { model | response = "Submitted! Tx: " ++ response }, Cmd.none )
Err _ ->
( { model | response = "Error submitting feedback" }, Cmd.none )
submitFeedback : String -> String -> Cmd Msg
submitFeedback proof feedback =
Http.post
{ url = "http://localhost:3000/submit-feedback"
, body = Http.jsonBody <|
Encode.object
[ ("proof", Encode.string proof)
, ("feedback", Encode.string feedback)
]
, expect = Http.expectString GotResponse
}
-- View
view : Model -> Html Msg
view model =
div []
[ div [] [ text "Proof:" ]
, input [ onInput UpdateProof ] []
, div [] [ text "Feedback:" ]
, input [ onInput UpdateFeedback ] []
, button [ onClick Submit ] [ text "Submit" ]
, div [] [ text model.response ]
]
-- Main
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> ( init, Cmd.none )
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}

27
ui/prototype/.gitignore vendored Normal file

@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# next.js
/.next/
/out/
# production
/build
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

@ -0,0 +1,273 @@
import Link from "next/link"
import {
ArrowRight,
CheckCircle,
FileText,
LightbulbIcon,
MessageSquare,
VoteIcon,
Database,
Lock,
Server,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
export default function AboutPage() {
return (
<div className="container mx-auto px-4 py-8">
<div className="mx-auto max-w-4xl">
<div className="mb-12 text-center">
<h1 className="text-4xl font-bold">About VoxPop</h1>
<p className="mt-4 text-lg text-muted-foreground">
Empowering citizens to shape legislation through collective wisdom
</p>
</div>
<Tabs defaultValue="platform" className="mb-12">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="platform">Our Platform</TabsTrigger>
<TabsTrigger value="process">Our Process</TabsTrigger>
<TabsTrigger value="technology">Our Technology</TabsTrigger>
</TabsList>
<TabsContent value="platform" className="mt-6">
<div className="space-y-4">
<h2 className="text-2xl font-semibold">Decentralized Public Opinion Platform</h2>
<p className="text-muted-foreground">
VoxPop is a decentralized platform that bridges the gap between citizens and policymakers. We believe
that the collective wisdom of communities should directly inform the laws that govern them.
</p>
<p className="text-muted-foreground">
Our mission is to create a transparent, accessible, and effective system for public participation in the
legislative process. By leveraging blockchain technology and artificial intelligence, we ensure that
every voice is heard and every contribution is valued.
</p>
<div className="mt-8">
<h3 className="text-xl font-medium">Key Benefits</h3>
<ul className="mt-4 space-y-2">
<li className="flex items-start">
<CheckCircle className="mr-2 mt-1 h-5 w-5 text-primary" />
<div>
<p className="font-medium">Transparent Governance</p>
<p className="text-sm text-muted-foreground">
All submissions and votes are recorded on the blockchain, ensuring complete transparency and
accountability.
</p>
</div>
</li>
<li className="flex items-start">
<CheckCircle className="mr-2 mt-1 h-5 w-5 text-primary" />
<div>
<p className="font-medium">Inclusive Participation</p>
<p className="text-sm text-muted-foreground">
Our platform is designed to be accessible to all citizens, regardless of technical expertise or
political connections.
</p>
</div>
</li>
<li className="flex items-start">
<CheckCircle className="mr-2 mt-1 h-5 w-5 text-primary" />
<div>
<p className="font-medium">Efficient Consensus Building</p>
<p className="text-sm text-muted-foreground">
AI-powered synthesis of perspectives helps identify common ground and build consensus around
complex issues.
</p>
</div>
</li>
</ul>
</div>
</div>
</TabsContent>
<TabsContent value="process" className="mt-6">
<h2 className="text-2xl font-semibold">The VoxPop Process</h2>
<p className="mt-2 text-muted-foreground">
Our platform follows a structured process to transform individual perspectives into actionable policy
recommendations.
</p>
<div className="mt-8 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<MessageSquare className="h-6 w-6 text-primary" />
</div>
<CardTitle className="mt-4">1. Perspective</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground">
Citizens submit their raw opinions, ideas, and concerns on specific issues or proposed legislation.
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<Server className="h-6 w-6 text-primary" />
</div>
<CardTitle className="mt-4">2. Synthesis</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground">
AI algorithms process and synthesize similar perspectives to identify common themes and potential
solutions.
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<LightbulbIcon className="h-6 w-6 text-primary" />
</div>
<CardTitle className="mt-4">3. Insight</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground">
Synthesized ideas become Insights that are presented to the community for review and voting.
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<VoteIcon className="h-6 w-6 text-primary" />
</div>
<CardTitle className="mt-4">4. Consensus</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground">
Community members vote on Insights to build consensus and identify the most widely supported ideas.
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<CheckCircle className="h-6 w-6 text-primary" />
</div>
<CardTitle className="mt-4">5. Contribution</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground">
Insights that reach community consensus become validated Contributions ready for policy
consideration.
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<FileText className="h-6 w-6 text-primary" />
</div>
<CardTitle className="mt-4">6. Resolution</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground">
Validated Contributions are compiled into formal Resolutions that can be presented to policymakers.
</p>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="technology" className="mt-6">
<h2 className="text-2xl font-semibold">Our Technology Stack</h2>
<p className="mt-2 text-muted-foreground">
VoxPop leverages cutting-edge technologies to ensure security, transparency, and efficiency.
</p>
<div className="mt-8 space-y-6">
<div className="rounded-lg border p-6">
<div className="flex items-start">
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-full bg-primary/10">
<Database className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="text-xl font-medium">Polygon PoS Blockchain</h3>
<p className="mt-2 text-muted-foreground">
We use the Polygon Proof-of-Stake blockchain for its energy efficiency, low transaction costs, and
high throughput. All votes, contributions, and resolutions are recorded on-chain for complete
transparency and immutability.
</p>
</div>
</div>
</div>
<div className="rounded-lg border p-6">
<div className="flex items-start">
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-full bg-primary/10">
<Server className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="text-xl font-medium">IPFS Storage</h3>
<p className="mt-2 text-muted-foreground">
The InterPlanetary File System (IPFS) provides decentralized storage for perspective submissions
and other content. This ensures that data cannot be censored or altered, and remains accessible
even if VoxPop's servers are unavailable.
</p>
</div>
</div>
</div>
<div className="rounded-lg border p-6">
<div className="flex items-start">
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-full bg-primary/10">
<Lock className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="text-xl font-medium">Privado ID</h3>
<p className="mt-2 text-muted-foreground">
Privado ID provides privacy-preserving identity verification using zero-knowledge proofs. This
allows users to prove their eligibility to participate without revealing personal information,
preventing both identity fraud and protecting user privacy.
</p>
</div>
</div>
</div>
<div className="rounded-lg border p-6">
<div className="flex items-start">
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-full bg-primary/10">
<LightbulbIcon className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="text-xl font-medium">AI Synthesis</h3>
<p className="mt-2 text-muted-foreground">
Our proprietary AI algorithms analyze and synthesize perspectives to identify common themes,
concerns, and potential solutions. This helps transform thousands of individual opinions into
actionable insights that can be effectively evaluated by the community.
</p>
</div>
</div>
</div>
</div>
</TabsContent>
</Tabs>
<div className="mt-12 text-center">
<h2 className="text-2xl font-semibold">Ready to make your voice heard?</h2>
<p className="mt-4 text-muted-foreground">
Join thousands of citizens who are already shaping the future of their communities through VoxPop.
</p>
<div className="mt-8 flex flex-col gap-4 sm:flex-row sm:justify-center">
<Button asChild size="lg" className="gap-2">
<Link href="/submit">
Submit Your Perspective
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
<Button asChild variant="outline" size="lg">
<Link href="/insights">Explore Insights</Link>
</Button>
</div>
</div>
</div>
</div>
)
}

@ -0,0 +1,50 @@
"use client"
import type React from "react"
import { useEffect } from "react"
import { Inter } from "next/font/google"
import "./globals.css"
import { ThemeProvider } from "@/components/theme-provider"
import { SidebarProvider } from "@/components/sidebar-provider"
import { AppSidebar } from "@/components/app-sidebar"
import { SidebarInset } from "@/components/ui/sidebar"
import { Breadcrumbs } from "@/components/breadcrumbs"
import { useWalletStore } from "@/lib/wallet-store"
const inter = Inter({ subsets: ["latin"] })
export default function ClientLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
// Check for wallet connection on initial load
const { walletConnected } = useWalletStore()
// Log wallet connection status for debugging
useEffect(() => {
console.log("Wallet connection status:", walletConnected)
}, [walletConnected])
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem disableTransitionOnChange>
<SidebarProvider>
<div className="flex min-h-screen">
<AppSidebar />
<SidebarInset className="flex-1">
<main className="flex-1">
<Breadcrumbs />
{children}
</main>
</SidebarInset>
</div>
</SidebarProvider>
</ThemeProvider>
</body>
</html>
)
}

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

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

@ -0,0 +1,4 @@
export default function Loading() {
return null
}

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

@ -0,0 +1,80 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 210 100% 50%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
/* Sidebar specific variables */
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 210 100% 50%;
--primary-foreground: 0 0% 98%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
/* Sidebar specific variables */
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 0 0% 98%;
--sidebar-primary-foreground: 240 5.9% 10%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

@ -0,0 +1,4 @@
export default function Loading() {
return null
}

@ -0,0 +1,810 @@
"use client"
import type React from "react"
import { useState, useRef, useEffect } from "react"
import {
ArrowUpDown,
Filter,
Search,
ThumbsDown,
ThumbsUp,
SortAsc,
SortDesc,
CheckCircle,
X,
Clock,
Check,
} 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 { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Progress } from "@/components/ui/progress"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { useWalletStore } from "@/lib/wallet-store"
import { cn } from "@/lib/utils"
import { toast } from "@/components/ui/use-toast"
import { ToastAction } from "@/components/ui/toast"
// Mock data for insights
const mockInsights = [
{
id: 1,
title: "Increase Park Funding",
description: "Allocate 15% more funding to local parks for maintenance and new facilities",
category: "Environmental Policy",
votes: { yes: 245, no: 32 },
status: "voting", // voting, consensus, rejected
dateAdded: "2024-02-15",
},
{
id: 2,
title: "Public Transportation Expansion",
description: "Extend bus routes to underserved neighborhoods and increase service frequency",
category: "Infrastructure",
votes: { yes: 189, no: 45 },
status: "voting",
dateAdded: "2024-02-10",
},
{
id: 3,
title: "After-School Programs",
description: "Create free after-school programs for K-8 students in public schools",
category: "Education",
votes: { yes: 312, no: 28 },
status: "consensus",
dateAdded: "2024-01-28",
},
{
id: 4,
title: "Community Health Clinics",
description: "Establish walk-in clinics in underserved areas with sliding scale fees",
category: "Healthcare",
votes: { yes: 278, no: 42 },
status: "consensus",
dateAdded: "2024-01-22",
},
{
id: 5,
title: "Affordable Housing Initiative",
description: "Require 20% affordable units in new residential developments over 50 units",
category: "Housing",
votes: { yes: 156, no: 98 },
status: "voting",
dateAdded: "2024-01-15",
},
{
id: 6,
title: "Bike Lane Network",
description: "Create a connected network of protected bike lanes throughout the city",
category: "Infrastructure",
votes: { yes: 203, no: 87 },
status: "voting",
dateAdded: "2024-01-05",
},
]
export default function InsightsDashboard() {
const [insights, setInsights] = useState(mockInsights)
const [activeTab, setActiveTab] = useState("all")
const [searchQuery, setSearchQuery] = useState("")
const [categoryFilter, setCategoryFilter] = useState("all")
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc")
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)
const [selectedInsights, setSelectedInsights] = useState<Record<number, "yes" | "no">>({})
const [isSubmittingBatch, setIsSubmittingBatch] = useState(false)
const [countdownSeconds, setCountdownSeconds] = useState(5)
const countdownRef = useRef<NodeJS.Timeout | null>(null)
const filteredInsights = insights.filter((insight) => {
// Filter by tab
if (activeTab === "consensus" && insight.status !== "consensus") return false
if (activeTab === "voting" && insight.status !== "voting") return false
// Filter by search query
if (
searchQuery &&
!insight.title.toLowerCase().includes(searchQuery.toLowerCase()) &&
!insight.description.toLowerCase().includes(searchQuery.toLowerCase())
)
return false
// Filter by category
if (categoryFilter !== "all" && insight.category !== categoryFilter) return false
return true
})
const sortInsights = (insights: typeof filteredInsights) => {
return [...insights].sort((a, b) => {
if (sortBy === "votes") {
const totalVotesA = a.votes.yes + a.votes.no
const totalVotesB = b.votes.yes + b.votes.no
return sortOrder === "desc" ? totalVotesB - totalVotesA : totalVotesA - totalVotesB
} else {
// Assuming each insight has a dateAdded property
const dateA = new Date(a.dateAdded || "2023-01-01").getTime()
const dateB = new Date(b.dateAdded || "2023-01-01").getTime()
return sortOrder === "desc" ? dateB - dateA : dateA - dateB
}
})
}
const sortedInsights = sortInsights(filteredInsights)
// Count selected insights
const selectedCount = Object.keys(selectedInsights).length
// Handle individual vote
const handleVote = (id: number, voteType: "yes" | "no") => {
// Record the user's vote
setUserVotes((prev) => ({
...prev,
[id]: voteType,
}))
// Update the insight's vote count
setInsights(
insights.map((insight) => {
if (insight.id === id) {
const updatedVotes = {
...insight.votes,
[voteType]: insight.votes[voteType] + 1,
}
// Check if consensus is reached (75% yes votes)
const totalVotes = updatedVotes.yes + updatedVotes.no
const status = updatedVotes.yes / totalVotes > 0.75 ? "consensus" : "voting"
return { ...insight, votes: updatedVotes, status }
}
return insight
}),
)
// Queue toast for individual votes (not batch mode)
if (!batchMode) {
setPendingToast({ type: "single", voteType, insightId: id })
}
}
// Toggle batch mode
const toggleBatchMode = () => {
if (batchMode) {
// Clear selections when exiting batch mode
setSelectedInsights({})
}
setBatchMode(!batchMode)
}
// Toggle selection of an insight in batch mode
const toggleSelection = (id: number, voteType: "yes" | "no") => {
// Find the insight
const insight = insights.find((i) => i.id === id)
// Don't allow selection if insight has reached consensus
if (insight?.status === "consensus") return
setSelectedInsights((prev) => {
const newSelections = { ...prev }
// If already selected with this vote type, remove it
if (newSelections[id] === voteType) {
delete newSelections[id]
} else {
// Otherwise add/update it
newSelections[id] = voteType
}
return newSelections
})
}
// Start batch submission with countdown
const startBatchSubmission = () => {
if (selectedCount === 0) return
setIsSubmittingBatch(true)
setCountdownSeconds(5)
// Start countdown
countdownRef.current = setInterval(() => {
setCountdownSeconds((prev) => {
if (prev <= 1) {
// Clear interval when countdown reaches 0
if (countdownRef.current) clearInterval(countdownRef.current)
// Process the batch
processBatchVotes()
return 0
}
return prev - 1
})
}, 1000)
}
// Cancel batch submission
const cancelBatchSubmission = () => {
if (countdownRef.current) {
clearInterval(countdownRef.current)
}
setIsSubmittingBatch(false)
}
// 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)
})
// Queue toast for batch submission
setPendingToast({ type: "batch", batchSize })
// Reset batch state
setSelectedInsights({})
setIsSubmittingBatch(false)
}
// Handle toast notifications
useEffect(() => {
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({
children,
onComplete,
disabled = false,
variant = "default",
className = "",
holdTime = 3000, // 3 seconds
insightId,
voteType,
}: {
children: React.ReactNode
onComplete: () => void
disabled?: boolean
variant?: "default" | "outline"
className?: string
holdTime?: number
insightId: number
voteType: "yes" | "no"
}) {
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 || localVoted
const isThisVote = userVotes[insightId] === voteType
// Reset completed state when the insight changes
useEffect(() => {
setCompleted(isThisVote)
}, [isThisVote])
const startHold = () => {
if (disabled || hasVoted) return
setIsHolding(true)
setCompleted(false)
startTimeRef.current = Date.now()
// Use requestAnimationFrame for smoother animation
const animate = () => {
const elapsed = Date.now() - startTimeRef.current
const newProgress = Math.min((elapsed / holdTime) * 100, 100)
setProgress(newProgress)
if (newProgress >= 100) {
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)
}, 300)
} else {
animationRef.current = requestAnimationFrame(animate)
}
}
animationRef.current = requestAnimationFrame(animate)
}
const endHold = () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
animationRef.current = null
}
if (timerRef.current) {
clearTimeout(timerRef.current)
timerRef.current = null
}
if (!completed) {
setIsHolding(false)
setProgress(0)
}
}
useEffect(() => {
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
if (timerRef.current) {
clearTimeout(timerRef.current)
}
}
}, [])
// Get the base button color based on variant
const baseColor =
variant === "default" ? "bg-primary text-primary-foreground" : "bg-background text-foreground border border-input"
// Get the success color
const successColor = "bg-green-500 text-white"
return (
<Button
variant={variant}
size="sm"
className={cn(
"relative overflow-hidden transition-all",
(completed || isThisVote) && "ring-2 ring-green-500 ring-opacity-50",
hasVoted && !isThisVote && "opacity-50",
!isHolding && !completed && !isThisVote && baseColor,
(completed || isThisVote) && successColor,
className,
)}
style={{
// Apply dynamic background color based on progress
...(isHolding && {
background: `linear-gradient(to right,
${variant === "default" ? "#10b981" : "#10b981"} ${progress}%,
${variant === "default" ? "hsl(var(--primary))" : "hsl(var(--background))"} ${progress}%)`,
borderColor: progress > 50 ? "#10b981" : "",
color: progress > 70 ? "white" : "",
}),
}}
disabled={disabled || !walletConnected || hasVoted}
onMouseDown={startHold}
onMouseUp={endHold}
onMouseLeave={endHold}
onTouchStart={startHold}
onTouchEnd={endHold}
onTouchCancel={endHold}
>
<div
className={cn(
"relative z-10 flex items-center justify-center w-full",
(completed || isThisVote) && "scale-110 transition-transform duration-200",
)}
>
{completed || isThisVote ? <CheckCircle className="h-6 w-6 animate-pulse text-white" /> : children}
</div>
</Button>
)
}
// Batch selection button component
function BatchSelectionButton({
children,
insightId,
voteType,
className = "",
variant = "default",
disabled = false,
}: {
children: React.ReactNode
insightId: number
voteType: "yes" | "no"
className?: string
variant?: "default" | "outline"
disabled?: boolean
}) {
// Check if this insight is selected with this vote type
const isSelected = selectedInsights[insightId] === voteType
// Check if this insight has already been voted on
const hasVoted = userVotes[insightId] !== undefined
// Base styles
const baseColor =
variant === "default" ? "bg-primary text-primary-foreground" : "bg-background text-foreground border border-input"
const selectedColor = voteType === "yes" ? "bg-green-500 text-white" : "bg-red-500 text-white"
return (
<Button
variant={variant}
size="sm"
className={cn(
"relative overflow-hidden transition-all",
isSelected && "ring-2 ring-opacity-50",
isSelected && voteType === "yes" && "ring-green-500",
isSelected && voteType === "no" && "ring-red-500",
hasVoted && "opacity-50 cursor-not-allowed",
disabled && "opacity-50 cursor-not-allowed",
!isSelected && !hasVoted && !disabled && baseColor,
isSelected && selectedColor,
className,
)}
disabled={hasVoted || isSubmittingBatch || disabled}
onClick={() => !hasVoted && !disabled && toggleSelection(insightId, voteType)}
>
<div
className={cn(
"relative z-10 flex items-center justify-center w-full",
isSelected && "scale-110 transition-transform duration-200",
)}
>
{isSelected ? <Check className="h-5 w-5 mr-1" /> : null}
{children}
</div>
</Button>
)
}
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold">Insights Dashboard</h1>
<p className="mt-2 text-muted-foreground">
Review and vote on AI-generated insights derived from community perspectives.
</p>
</div>
{/* Batch Mode Toggle */}
{walletConnected && (
<div className="mb-6 flex items-center justify-between bg-muted/30 p-3 rounded-lg">
<div className="flex items-center space-x-2">
<Switch
id="batch-mode"
checked={batchMode}
onCheckedChange={toggleBatchMode}
disabled={isSubmittingBatch}
/>
<Label htmlFor="batch-mode" className="font-medium">
Batch Voting Mode
</Label>
<span className="text-sm text-muted-foreground">
{batchMode ? "Select multiple insights to vote on at once" : "Vote on insights individually"}
</span>
</div>
{batchMode && selectedCount > 0 && (
<div className="flex items-center gap-2">
<Badge variant="outline" className="bg-background">
{selectedCount} selected
</Badge>
{isSubmittingBatch ? (
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Submitting in {countdownSeconds}s</span>
<Button size="sm" variant="destructive" onClick={cancelBatchSubmission}>
<X className="h-4 w-4 mr-1" />
Cancel
</Button>
</div>
) : (
<Button size="sm" onClick={startBatchSubmission}>
Submit Votes ({selectedCount})
</Button>
)}
</div>
)}
</div>
)}
{/* Filters and Search */}
<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="relative w-full md:w-72">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search insights..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex flex-col gap-4 sm:flex-row">
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger className="w-full sm:w-[180px]">
<Filter className="mr-2 h-4 w-4" />
<SelectValue placeholder="Filter by category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
<SelectItem value="Environmental Policy">Environmental</SelectItem>
<SelectItem value="Infrastructure">Infrastructure</SelectItem>
<SelectItem value="Education">Education</SelectItem>
<SelectItem value="Healthcare">Healthcare</SelectItem>
<SelectItem value="Housing">Housing</SelectItem>
</SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="flex items-center gap-2">
<ArrowUpDown className="h-4 w-4" />
Sort by {sortBy === "votes" ? "Votes" : "Date"}
{sortOrder === "desc" ? <SortDesc className="ml-1 h-4 w-4" /> : <SortAsc className="ml-1 h-4 w-4" />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setSortBy("votes")}>Votes {sortBy === "votes" && "✓"}</DropdownMenuItem>
<DropdownMenuItem onClick={() => setSortBy("date")}>Date {sortBy === "date" && "✓"}</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setSortOrder(sortOrder === "desc" ? "asc" : "desc")}>
{sortOrder === "desc" ? "Ascending" : "Descending"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* Tabs */}
<Tabs defaultValue="all" value={activeTab} onValueChange={setActiveTab} className="mb-6">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="all">All Insights</TabsTrigger>
<TabsTrigger value="voting">Voting Open</TabsTrigger>
<TabsTrigger value="consensus">Consensus Reached</TabsTrigger>
</TabsList>
</Tabs>
{!walletConnected && (
<div className="mb-6 rounded-lg border border-amber-200 bg-amber-50 p-4 text-amber-800 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200">
<p className="text-sm font-medium">Connect your wallet to vote on insights</p>
<p className="mt-1 text-xs">Your vote helps shape community consensus and policy decisions</p>
</div>
)}
{/* Batch submission overlay */}
{isSubmittingBatch && (
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center">
<div className="bg-card p-6 rounded-lg shadow-lg max-w-md w-full">
<div className="text-center">
<Clock className="h-12 w-12 mx-auto mb-4 text-primary animate-pulse" />
<h2 className="text-xl font-bold mb-2">Submitting Votes</h2>
<p className="text-muted-foreground mb-4">
Your votes will be submitted in {countdownSeconds} seconds. You can cancel if you need to make changes.
</p>
<div className="mb-4">
<Progress value={(5 - countdownSeconds) * 20} className="h-2" />
</div>
<div className="flex flex-col gap-4 max-h-40 overflow-y-auto mb-4">
{Object.entries(selectedInsights).map(([idStr, voteType]) => {
const id = Number.parseInt(idStr)
const insight = insights.find((i) => i.id === id)
if (!insight) return null
return (
<div key={id} className="flex items-center justify-between text-sm p-2 bg-muted/30 rounded">
<span className="truncate max-w-[200px]">{insight.title}</span>
<Badge className={voteType === "yes" ? "bg-green-500" : "bg-red-500"}>
{voteType === "yes" ? "Yes" : "No"}
</Badge>
</div>
)
})}
</div>
<div className="flex gap-4 justify-center">
<Button variant="outline" onClick={cancelBatchSubmission}>
<X className="mr-2 h-4 w-4" />
Cancel
</Button>
<Button onClick={processBatchVotes}>
<Check className="mr-2 h-4 w-4" />
Submit Now
</Button>
</div>
</div>
</div>
</div>
)}
{batchMode && (
<div className="mb-4 text-sm text-muted-foreground">
<p>Note: Insights that have already reached consensus cannot be selected for batch voting.</p>
</div>
)}
{/* Insights Grid */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{sortedInsights.map((insight) => (
<Card
key={insight.id}
className={cn(
"flex flex-col transition-all duration-200",
selectedInsights[insight.id] === "yes" && "ring-2 ring-green-500",
selectedInsights[insight.id] === "no" && "ring-2 ring-red-500",
isSubmittingBatch && selectedInsights[insight.id] && "animate-pulse",
)}
>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle>{insight.title}</CardTitle>
<CardDescription className="mt-1">{insight.category}</CardDescription>
</div>
<Badge
variant={insight.status === "consensus" ? "default" : "outline"}
className={
insight.status === "voting"
? "bg-amber-100 text-amber-800 hover:bg-amber-200 dark:bg-amber-900/60 dark:text-amber-100 dark:hover:bg-amber-800"
: ""
}
>
{insight.status === "consensus" ? "Consensus" : "Voting"}
</Badge>
</div>
</CardHeader>
<CardContent className="flex-1">
<p className="text-sm text-muted-foreground">{insight.description}</p>
<div className="mt-4">
<div className="mb-1 flex items-center justify-between text-xs">
<span>Yes: {insight.votes.yes}</span>
<span>No: {insight.votes.no}</span>
</div>
<Progress value={(insight.votes.yes / (insight.votes.yes + insight.votes.no)) * 100} className="h-2" />
</div>
</CardContent>
<CardFooter className="flex justify-between">
{batchMode ? (
// Batch mode voting buttons
<>
<BatchSelectionButton
variant="outline"
className="w-[48%]"
insightId={insight.id}
voteType="no"
disabled={insight.status === "consensus"}
>
<ThumbsDown className="mr-2 h-4 w-4" />
No
</BatchSelectionButton>
<BatchSelectionButton
className="w-[48%]"
insightId={insight.id}
voteType="yes"
disabled={insight.status === "consensus"}
>
<ThumbsUp className="mr-2 h-4 w-4" />
Yes
</BatchSelectionButton>
</>
) : (
// Individual voting buttons
<>
<HoldButton
variant="outline"
className="w-[48%]"
onComplete={() => handleVote(insight.id, "no")}
disabled={insight.status === "consensus"}
insightId={insight.id}
voteType="no"
>
<ThumbsDown className="mr-2 h-4 w-4" />
No
</HoldButton>
<HoldButton
className="w-[48%]"
onComplete={() => handleVote(insight.id, "yes")}
disabled={insight.status === "consensus"}
insightId={insight.id}
voteType="yes"
>
<ThumbsUp className="mr-2 h-4 w-4" />
Yes
</HoldButton>
</>
)}
</CardFooter>
</Card>
))}
</div>
{filteredInsights.length === 0 && (
<div className="mt-12 text-center">
<p className="text-lg font-medium">No insights match your filters</p>
<p className="mt-2 text-muted-foreground">Try adjusting your search or filters</p>
<Button
variant="outline"
className="mt-4"
onClick={() => {
setSearchQuery("")
setCategoryFilter("all")
setActiveTab("all")
}}
>
Clear All Filters
</Button>
</div>
)}
{/* Floating batch submission button for mobile */}
{batchMode && selectedCount > 0 && !isSubmittingBatch && (
<div className="fixed bottom-4 right-4 md:hidden">
<Button size="lg" onClick={startBatchSubmission} className="shadow-lg">
Submit Votes ({selectedCount})
</Button>
</div>
)}
</div>
)
}

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

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

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

@ -0,0 +1,31 @@
import type React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import ClientLayout from "./client-layout"
import { Toaster } from "@/components/toaster"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
generator: 'v0.dev'
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClientLayout>
{children}
<Toaster />
</ClientLayout>
)
}
import './globals.css'

201
ui/prototype/app/page.tsx Normal file

@ -0,0 +1,201 @@
import Link from "next/link"
import { ArrowRight, FileText, LightbulbIcon, MessageSquare, VoteIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { CountUpAnimation } from "@/components/count-up-animation"
export default function Home() {
return (
<div className="container mx-auto px-4 py-8">
{/* Hero Section */}
<section className="py-12 md:py-24">
<div className="mx-auto max-w-4xl text-center">
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">Your Voice, Your Laws</h1>
<p className="mt-6 text-lg text-muted-foreground md:text-xl">
VoxPop empowers citizens to shape legislation through collective wisdom. Submit your perspectives, vote on
insights, and influence policy decisions.
</p>
<div className="mt-10 flex flex-col gap-4 sm:flex-row sm:justify-center">
<Button asChild size="lg" className="gap-2">
<Link href="/submit">
Join Now
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
<Button asChild variant="outline" size="lg">
<Link href="/insights">Explore Insights</Link>
</Button>
</div>
</div>
</section>
{/* Process Timeline */}
<section className="py-12">
<div className="mx-auto max-w-5xl">
<h2 className="mb-8 text-center text-3xl font-bold">How VoxPop Works</h2>
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
<Card>
<CardHeader className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<MessageSquare className="h-6 w-6 text-primary" />
</div>
<CardTitle className="mt-4">Submit Perspective</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground">
Share your opinion on issues that matter to you. Your voice is the foundation of better policy.
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<LightbulbIcon className="h-6 w-6 text-primary" />
</div>
<CardTitle className="mt-4">Vote on Insights</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground">
Review AI-synthesized insights and vote on the best ideas from the community to build consensus.
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
<FileText className="h-6 w-6 text-primary" />
</div>
<CardTitle className="mt-4">Create Resolutions</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground">
Validated contributions become formal resolutions that can be presented to policymakers.
</p>
</CardContent>
</Card>
</div>
<div className="mt-12 flex justify-center">
<Button asChild variant="outline">
<Link href="/about">Learn More About Our Process</Link>
</Button>
</div>
</div>
</section>
{/* Stats Dashboard */}
<section className="py-12 bg-muted/50 rounded-lg my-12">
<div className="mx-auto max-w-5xl px-4">
<h2 className="mb-8 text-center text-3xl font-bold">Community Impact</h2>
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
<div className="flex flex-col items-center">
<CountUpAnimation end={1234} className="text-4xl font-bold text-primary" prefix="" suffix="" />
<p className="mt-2 text-sm text-muted-foreground">Perspectives Submitted</p>
<div className="mt-3 h-2 w-3/4 overflow-hidden rounded-full bg-primary/20">
<div className="h-full w-[85%] animate-pulse rounded-full bg-primary"></div>
</div>
</div>
<div className="flex flex-col items-center">
<CountUpAnimation end={567} className="text-4xl font-bold text-primary" prefix="" suffix="" />
<p className="mt-2 text-sm text-muted-foreground">Insights Generated</p>
<div className="mt-3 h-2 w-3/4 overflow-hidden rounded-full bg-primary/20">
<div className="h-full w-[65%] animate-pulse rounded-full bg-primary"></div>
</div>
</div>
<div className="flex flex-col items-center">
<CountUpAnimation end={89} className="text-4xl font-bold text-primary" prefix="" suffix="" />
<p className="mt-2 text-sm text-muted-foreground">Contributions Validated</p>
<div className="mt-3 h-2 w-3/4 overflow-hidden rounded-full bg-primary/20">
<div className="h-full w-[45%] animate-pulse rounded-full bg-primary"></div>
</div>
</div>
<div className="flex flex-col items-center">
<CountUpAnimation end={12} className="text-4xl font-bold text-primary" prefix="" suffix="" />
<p className="mt-2 text-sm text-muted-foreground">Resolutions Delivered</p>
<div className="mt-3 h-2 w-3/4 overflow-hidden rounded-full bg-primary/20">
<div className="h-full w-[25%] animate-pulse rounded-full bg-primary"></div>
</div>
</div>
</div>
</div>
</section>
{/* Featured Insights */}
<section className="py-12">
<div className="mx-auto max-w-5xl">
<h2 className="mb-8 text-center text-3xl font-bold">Featured Insights</h2>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Increase Park Funding</CardTitle>
<CardDescription>Environmental Policy</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Community members support increasing funding for local parks by 15% to improve maintenance and add new
recreational facilities.
</p>
</CardContent>
<CardFooter className="flex justify-between">
<div className="flex items-center text-sm text-muted-foreground">
<VoteIcon className="mr-1 h-4 w-4" />
<span>245 votes</span>
</div>
<Button variant="outline" size="sm" asChild>
<Link href="/insights/park-funding">View Details</Link>
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>Public Transportation Expansion</CardTitle>
<CardDescription>Infrastructure</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Residents propose extending bus routes to underserved neighborhoods and increasing service frequency
during peak hours.
</p>
</CardContent>
<CardFooter className="flex justify-between">
<div className="flex items-center text-sm text-muted-foreground">
<VoteIcon className="mr-1 h-4 w-4" />
<span>189 votes</span>
</div>
<Button variant="outline" size="sm" asChild>
<Link href="/insights/transportation">View Details</Link>
</Button>
</CardFooter>
</Card>
</div>
<div className="mt-8 flex justify-center">
<Button asChild>
<Link href="/insights">
View All Insights
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
</section>
{/* Call to Action */}
<section className="py-12 md:py-24">
<div className="mx-auto max-w-3xl text-center">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Ready to make your voice heard?</h2>
<p className="mt-6 text-lg text-muted-foreground">
Join thousands of citizens who are already shaping the future of their communities through VoxPop.
</p>
<div className="mt-10 flex flex-col gap-4 sm:flex-row sm:justify-center">
<Button asChild size="lg" className="gap-2">
<Link href="/submit">
Submit Your Perspective
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
</div>
</div>
</section>
</div>
)
}

@ -0,0 +1,253 @@
"use client"
import { useState } from "react"
import { ArrowRight, CheckCircle, Clock, Edit, MessageSquare, VoteIcon } 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 { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { ConnectWalletButton } from "@/components/connect-wallet-button"
// Mock data
const mockPerspectives = [
{
id: 1,
title: "Improve bike lane safety",
issue: "Infrastructure",
date: "2024-02-10",
status: "Synthesized",
},
{
id: 2,
title: "Expand library hours",
issue: "Education",
date: "2024-01-25",
status: "Synthesized",
},
{
id: 3,
title: "Community garden proposal",
issue: "Environmental Policy",
date: "2024-01-15",
status: "Pending",
},
]
const mockVotes = [
{
id: 1,
insight: "Increase Park Funding",
vote: "Yes",
date: "2024-02-15",
},
{
id: 2,
insight: "Public Transportation Expansion",
vote: "Yes",
date: "2024-02-10",
},
{
id: 3,
insight: "After-School Programs",
vote: "Yes",
date: "2024-01-28",
},
{
id: 4,
insight: "Bike Lane Network",
vote: "No",
date: "2024-01-20",
},
]
export default function ProfilePage() {
const [walletConnected, setWalletConnected] = useState(false)
const [delegateAddress, setDelegateAddress] = useState("")
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold">Profile</h1>
<p className="mt-2 text-muted-foreground">Manage your participation and delegation settings</p>
</div>
{!walletConnected ? (
<Card className="mx-auto max-w-md">
<CardHeader>
<CardTitle>Connect Your Wallet</CardTitle>
<CardDescription>Connect your wallet to view your profile and participation history</CardDescription>
</CardHeader>
<CardContent className="flex justify-center py-6">
<ConnectWalletButton onConnect={() => setWalletConnected(true)} />
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
{/* Profile Card */}
<Card className="lg:col-span-1">
<CardHeader>
<CardTitle>Your Profile</CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center">
<Avatar className="h-24 w-24">
<AvatarFallback className="text-xl">VP</AvatarFallback>
</Avatar>
<div className="mt-4 text-center">
<h3 className="text-lg font-medium">VoxPop User</h3>
<p className="text-sm text-muted-foreground">0x1a2...3b4c</p>
</div>
<div className="mt-6 grid w-full grid-cols-2 gap-4 text-center">
<div>
<p className="text-2xl font-bold">{mockPerspectives.length}</p>
<p className="text-sm text-muted-foreground">Perspectives</p>
</div>
<div>
<p className="text-2xl font-bold">{mockVotes.length}</p>
<p className="text-sm text-muted-foreground">Votes</p>
</div>
</div>
<div className="mt-6 w-full">
<Button variant="outline" className="w-full" asChild>
<a href="/settings">
<Edit className="mr-2 h-4 w-4" />
Edit Profile
</a>
</Button>
</div>
</CardContent>
</Card>
{/* Activity Tabs */}
<div className="lg:col-span-2">
<Tabs defaultValue="perspectives">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="perspectives">Your Perspectives</TabsTrigger>
<TabsTrigger value="votes">Your Votes</TabsTrigger>
</TabsList>
<TabsContent value="perspectives" className="mt-6">
<div className="rounded-lg border">
{mockPerspectives.map((perspective, index) => (
<div
key={perspective.id}
className={`flex items-center justify-between p-4 ${
index !== mockPerspectives.length - 1 ? "border-b" : ""
}`}
>
<div className="flex items-start gap-3">
<div className="rounded-full bg-primary/10 p-2">
<MessageSquare className="h-4 w-4 text-primary" />
</div>
<div>
<p className="font-medium">{perspective.title}</p>
<p className="text-sm text-muted-foreground">
{perspective.issue} {new Date(perspective.date).toLocaleDateString()}
</p>
</div>
</div>
<Badge variant={perspective.status === "Synthesized" ? "default" : "outline"}>
{perspective.status === "Synthesized" ? (
<CheckCircle className="mr-1 h-3 w-3" />
) : (
<Clock className="mr-1 h-3 w-3" />
)}
{perspective.status}
</Badge>
</div>
))}
</div>
<div className="mt-4 flex justify-center">
<Button asChild>
<a href="/submit">
Submit New Perspective
<ArrowRight className="ml-2 h-4 w-4" />
</a>
</Button>
</div>
</TabsContent>
<TabsContent value="votes" className="mt-6">
<div className="rounded-lg border">
{mockVotes.map((vote, index) => (
<div
key={vote.id}
className={`flex items-center justify-between p-4 ${
index !== mockVotes.length - 1 ? "border-b" : ""
}`}
>
<div className="flex items-start gap-3">
<div className="rounded-full bg-primary/10 p-2">
<VoteIcon className="h-4 w-4 text-primary" />
</div>
<div>
<p className="font-medium">{vote.insight}</p>
<p className="text-sm text-muted-foreground">
Voted {vote.vote} {new Date(vote.date).toLocaleDateString()}
</p>
</div>
</div>
<Badge variant={vote.vote === "Yes" ? "default" : "destructive"}>{vote.vote}</Badge>
</div>
))}
</div>
<div className="mt-4 flex justify-center">
<Button asChild>
<a href="/insights">
View More Insights
<ArrowRight className="ml-2 h-4 w-4" />
</a>
</Button>
</div>
</TabsContent>
</Tabs>
</div>
{/* Delegation Settings */}
<Card className="lg:col-span-3">
<CardHeader>
<CardTitle>Delegation Settings</CardTitle>
<CardDescription>
Delegate your voting power to trusted representatives (liquid democracy)
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2">
<div>
<Label htmlFor="delegate-address">Delegate Address</Label>
<div className="mt-1.5 flex gap-2">
<Input
id="delegate-address"
placeholder="0x..."
value={delegateAddress}
onChange={(e) => setDelegateAddress(e.target.value)}
/>
<Button variant="outline" disabled={!delegateAddress}>
Delegate
</Button>
</div>
<p className="mt-2 text-xs text-muted-foreground">
Enter the wallet address of the person you want to delegate your votes to
</p>
</div>
<div className="rounded-lg border p-4">
<h3 className="font-medium">Current Delegation</h3>
<p className="mt-2 text-sm text-muted-foreground">
You are not currently delegating your votes to anyone.
</p>
<p className="mt-4 text-xs text-muted-foreground">
When you delegate your votes, the delegate can vote on your behalf, but you can always override
their vote.
</p>
</div>
</div>
</CardContent>
</Card>
</div>
)}
</div>
)
}

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

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

@ -0,0 +1,4 @@
export default function Loading() {
return null
}

@ -0,0 +1,310 @@
"use client"
import { useState } from "react"
import Link from "next/link"
import { ArrowRight, ArrowUpDown, Download, FileText, Filter, Search, SortAsc, SortDesc } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Separator } from "@/components/ui/separator"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
// Mock data for resolutions
const mockResolutions = [
{
id: 12,
title: "Education and Community Health Improvements",
description: "A comprehensive set of recommendations for improving education access and community health services",
date: "2024-02-15",
contributions: 8,
categories: ["Education", "Healthcare"],
status: "active",
},
{
id: 11,
title: "Environmental Protection and Green Spaces",
description: "Recommendations for preserving and expanding green spaces and environmental protections",
date: "2024-01-10",
contributions: 6,
categories: ["Environmental Policy"],
status: "active",
},
{
id: 10,
title: "Public Transportation and Infrastructure",
description: "Proposals for improving public transportation access and infrastructure maintenance",
date: "2023-12-05",
contributions: 7,
categories: ["Infrastructure"],
status: "archived",
},
]
// Add state for search, filter, and sort
export default function ResolutionsPage() {
const [searchQuery, setSearchQuery] = useState("")
const [categoryFilter, setCategoryFilter] = useState("all")
const [statusFilter, setStatusFilter] = useState("all")
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc")
const [sortBy, setSortBy] = useState<"contributions" | "date">("date")
// Filter and sort the resolutions
const filteredResolutions = mockResolutions.filter((resolution) => {
// Filter by search query
if (
searchQuery &&
!resolution.title.toLowerCase().includes(searchQuery.toLowerCase()) &&
!resolution.description.toLowerCase().includes(searchQuery.toLowerCase())
)
return false
// Filter by category
if (categoryFilter !== "all" && !resolution.categories.includes(categoryFilter)) return false
// Filter by status
if (statusFilter !== "all" && resolution.status !== statusFilter) return false
return true
})
// Sort the filtered resolutions
const sortedResolutions = [...filteredResolutions].sort((a, b) => {
if (sortBy === "contributions") {
return sortOrder === "desc" ? b.contributions - a.contributions : a.contributions - b.contributions
} else {
const dateA = new Date(a.date).getTime()
const dateB = new Date(b.date).getTime()
return sortOrder === "desc" ? dateB - dateA : dateA - dateB
}
})
// Group resolutions by status
const activeResolutions = sortedResolutions.filter((r) => r.status === "active")
const archivedResolutions = sortedResolutions.filter((r) => r.status === "archived")
// Add the search, filter, and sort UI
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold">Resolutions</h1>
<p className="mt-2 text-muted-foreground">
Final policy recommendations compiled from validated community contributions.
</p>
</div>
{/* Filters and Search */}
<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="relative w-full md:w-72">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search resolutions..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex flex-col gap-4 sm:flex-row">
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger className="w-full sm:w-[180px]">
<Filter className="mr-2 h-4 w-4" />
<SelectValue placeholder="Filter by category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
<SelectItem value="Education">Education</SelectItem>
<SelectItem value="Healthcare">Healthcare</SelectItem>
<SelectItem value="Environmental Policy">Environmental</SelectItem>
<SelectItem value="Infrastructure">Infrastructure</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="archived">Archived</SelectItem>
</SelectContent>
</Select>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="flex items-center gap-2">
<ArrowUpDown className="h-4 w-4" />
Sort by {sortBy === "contributions" ? "Contributions" : "Date"}
{sortOrder === "desc" ? <SortDesc className="ml-1 h-4 w-4" /> : <SortAsc className="ml-1 h-4 w-4" />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setSortBy("contributions")}>
Contributions {sortBy === "contributions" && "✓"}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setSortBy("date")}>Date {sortBy === "date" && "✓"}</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setSortOrder(sortOrder === "desc" ? "asc" : "desc")}>
{sortOrder === "desc" ? "Ascending" : "Descending"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{filteredResolutions.length === 0 ? (
<div className="mt-12 text-center">
<p className="text-lg font-medium">No resolutions match your filters</p>
<p className="mt-2 text-muted-foreground">Try adjusting your search or filters</p>
<Button
variant="outline"
className="mt-4"
onClick={() => {
setSearchQuery("")
setCategoryFilter("all")
setStatusFilter("all")
}}
>
Clear All Filters
</Button>
</div>
) : (
<>
{statusFilter !== "archived" && activeResolutions.length > 0 && (
<>
<div className="mb-8">
<h2 className="text-xl font-semibold">Active Resolutions</h2>
<p className="text-sm text-muted-foreground">
Current resolutions available for policymakers and the public
</p>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{activeResolutions.map((resolution) => (
<Card key={resolution.id}>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5 text-primary" />
Resolution #{resolution.id}
</CardTitle>
<CardDescription className="mt-1">{resolution.title}</CardDescription>
</div>
<Badge>Active</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{resolution.description}</p>
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div>
<p className="font-medium">Published</p>
<p className="text-muted-foreground">{new Date(resolution.date).toLocaleDateString()}</p>
</div>
<div>
<p className="font-medium">Contributions</p>
<p className="text-muted-foreground">{resolution.contributions} included</p>
</div>
</div>
<div className="mt-4">
<p className="text-sm font-medium">Categories</p>
<div className="mt-2 flex flex-wrap gap-2">
{resolution.categories.map((category) => (
<Badge key={category} variant="outline">
{category}
</Badge>
))}
</div>
</div>
</CardContent>
<CardFooter className="flex flex-col gap-4 sm:flex-row">
<Button variant="outline" className="w-full sm:w-auto" asChild>
<Link href={`/resolutions/${resolution.id}`}>
View Details
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
<Button className="w-full sm:w-auto">
<Download className="mr-2 h-4 w-4" />
Download PDF
</Button>
</CardFooter>
</Card>
))}
</div>
</>
)}
{statusFilter !== "active" && archivedResolutions.length > 0 && (
<>
<Separator className="my-8" />
<div className="mb-8">
<h2 className="text-xl font-semibold">Archive</h2>
<p className="text-sm text-muted-foreground">
Past resolutions that have been delivered to policymakers
</p>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{archivedResolutions.map((resolution) => (
<Card key={resolution.id} className="opacity-80">
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Resolution #{resolution.id}
</CardTitle>
<CardDescription className="mt-1">{resolution.title}</CardDescription>
</div>
<Badge variant="outline">Archived</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{resolution.description}</p>
<div className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div>
<p className="font-medium">Published</p>
<p className="text-muted-foreground">{new Date(resolution.date).toLocaleDateString()}</p>
</div>
<div>
<p className="font-medium">Contributions</p>
<p className="text-muted-foreground">{resolution.contributions} included</p>
</div>
</div>
</CardContent>
<CardFooter className="flex flex-col gap-4 sm:flex-row">
<Button variant="outline" className="w-full sm:w-auto" asChild>
<Link href={`/resolutions/${resolution.id}`}>
View Details
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
<Button variant="secondary" className="w-full sm:w-auto">
<Download className="mr-2 h-4 w-4" />
Download PDF
</Button>
</CardFooter>
</Card>
))}
</div>
</>
)}
</>
)}
</div>
)
}

@ -0,0 +1,327 @@
"use client"
import type React from "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 { useWalletStore } from "@/lib/wallet-store"
import { Badge } from "@/components/ui/badge"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
// 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("")
// 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) {
// Handle wallet connection requirement
return
}
if (!selectedIssue) {
// Issue selection is required
return
}
setIsSubmitting(true)
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)
} 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-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 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>
<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>
{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>
<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>
)}
</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>
)
}

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

@ -0,0 +1,244 @@
"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
import {
BarChart3,
FileText,
Home,
LightbulbIcon,
LogOut,
MessageSquare,
Settings,
User,
VoteIcon,
Info,
ExternalLink,
History,
Bell,
Twitter,
Github,
Linkedin,
ClipboardList,
} from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarSeparator,
SidebarTrigger,
} from "@/components/ui/sidebar"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { ConnectWalletButton } from "@/components/connect-wallet-button"
import { useWalletStore } from "@/lib/wallet-store"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Button } from "@/components/ui/button"
export function AppSidebar() {
const pathname = usePathname()
const { walletConnected, walletAddress, username, disconnectWallet } = useWalletStore()
const isActive = (path: string) => {
if (path === '/') {
return pathname === path
}
return pathname?.startsWith(path)
}
const mainNavItems = [
{
title: "Home",
icon: Home,
href: "/",
},
{
title: "Issues",
icon: ClipboardList,
href: "/issues",
},
{
title: "Submit Perspective",
icon: MessageSquare,
href: "/submit",
},
{
title: "Insights Dashboard",
icon: LightbulbIcon,
href: "/insights",
},
{
title: "Contributions",
icon: VoteIcon,
href: "/contributions",
},
{
title: "Resolutions",
icon: FileText,
href: "/resolutions",
},
{
title: "About",
icon: Info,
href: "/about",
},
]
const userNavItems = [
{
title: "Profile",
icon: User,
href: "/profile",
},
{
title: "Settings",
icon: Settings,
href: "/settings",
},
]
return (
<Sidebar variant="inset">
<SidebarHeader className="flex flex-col gap-2 p-4">
<div className="flex items-center gap-2">
<Link href="/" className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-primary-foreground">
<BarChart3 className="h-4 w-4" />
</div>
<span className="text-lg font-bold">VoxPop</span>
</Link>
<SidebarTrigger className="ml-auto" />
</div>
<div className="mt-2">
{walletConnected ? (
<div className="rounded-md border p-3">
<div className="flex items-center gap-2">
<Avatar className="h-8 w-8">
<AvatarFallback>{username?.[0] || "V"}</AvatarFallback>
</Avatar>
<div className="flex flex-col overflow-hidden">
<span className="truncate font-medium">{username || "VoxPop User"}</span>
<span className="truncate text-xs text-muted-foreground">{walletAddress}</span>
</div>
</div>
<div className="mt-3 flex flex-wrap gap-2">
<Button variant="outline" size="sm" className="h-7 w-7 p-0" asChild>
<Link href="/profile/social">
<Twitter className="h-3.5 w-3.5" />
<span className="sr-only">Twitter</span>
</Link>
</Button>
<Button variant="outline" size="sm" className="h-7 w-7 p-0" asChild>
<Link href="/profile/social">
<Github className="h-3.5 w-3.5" />
<span className="sr-only">Github</span>
</Link>
</Button>
<Button variant="outline" size="sm" className="h-7 w-7 p-0" asChild>
<Link href="/profile/social">
<Linkedin className="h-3.5 w-3.5" />
<span className="sr-only">LinkedIn</span>
</Link>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="ml-auto h-7">
<span className="sr-only">More options</span>
<ExternalLink className="h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Account</DropdownMenuLabel>
<DropdownMenuItem asChild>
<Link href="/profile/transactions">
<History className="mr-2 h-4 w-4" />
Transaction History
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/profile/notifications">
<Bell className="mr-2 h-4 w-4" />
Notifications
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={disconnectWallet} className="text-destructive focus:text-destructive">
<LogOut className="mr-2 h-4 w-4" />
Disconnect
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
) : (
<ConnectWalletButton />
)}
</div>
</SidebarHeader>
<SidebarSeparator />
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{mainNavItems.map((item) => (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton asChild isActive={isActive(item.href)} tooltip={item.title}>
<Link href={item.href}>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarSeparator />
<SidebarGroup>
<SidebarGroupLabel>User</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{userNavItems.map((item) => (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton asChild isActive={isActive(item.href)} tooltip={item.title}>
<Link href={item.href}>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
{walletConnected && (
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton onClick={disconnectWallet}>
<LogOut className="mr-2 h-4 w-4" />
<span>Disconnect</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
)}
</Sidebar>
)
}

@ -0,0 +1,47 @@
"use client"
import { usePathname } from "next/navigation"
import Link from "next/link"
import { ChevronRight, Home } from "lucide-react"
export function Breadcrumbs() {
const pathname = usePathname()
// Skip breadcrumbs on home page
if (pathname === "/") return null
// Split the pathname into segments
const segments = pathname.split("/").filter(Boolean)
// Create breadcrumb items
const breadcrumbs = [
{ name: "Home", href: "/" },
...segments.map((segment, index) => {
const href = `/${segments.slice(0, index + 1).join("/")}`
// Format the segment name (capitalize first letter, replace hyphens with spaces)
const name = segment.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())
return { name, href }
}),
]
return (
<nav className="flex px-4 py-2 text-sm text-muted-foreground">
<ol className="flex items-center space-x-1">
{breadcrumbs.map((breadcrumb, index) => (
<li key={breadcrumb.href} className="flex items-center">
{index > 0 && <ChevronRight className="mx-1 h-4 w-4" />}
{index === breadcrumbs.length - 1 ? (
<span className="font-medium text-foreground">{breadcrumb.name}</span>
) : (
<Link href={breadcrumb.href} className="hover:text-foreground hover:underline">
{index === 0 ? <Home className="h-4 w-4" /> : breadcrumb.name}
</Link>
)}
</li>
))}
</ol>
</nav>
)
}

@ -0,0 +1,10 @@
"use client"
import { Wallet } from "lucide-react"
import { Button } from "@/components/ui/button"
import { WalletConnectionModal } from "@/components/wallet-connection-modal"
export function ConnectWalletButton() {
return <WalletConnectionModal />
}

@ -0,0 +1,91 @@
"use client"
import { useState, useEffect, useRef } from "react"
import { useInView } from "react-intersection-observer"
import { cn } from "@/lib/utils"
interface CountUpAnimationProps {
end: number
start?: number
duration?: number
delay?: number
prefix?: string
suffix?: string
decimals?: number
className?: string
}
export function CountUpAnimation({
end,
start = 0,
duration = 2000,
delay = 0,
prefix = "",
suffix = "",
decimals = 0,
className,
}: CountUpAnimationProps) {
const [count, setCount] = useState(start)
const countRef = useRef<number>(start)
const [ref, inView] = useInView({
triggerOnce: true,
threshold: 0.1,
})
useEffect(() => {
let startTime: number | null = null
let animationFrame: number | null = null
// Only start animation when in view and after delay
if (!inView) return
const timeout = setTimeout(() => {
const animate = (timestamp: number) => {
if (!startTime) startTime = timestamp
const progress = Math.min((timestamp - startTime) / duration, 1)
// Use easeOutExpo for a nice deceleration effect
const easeOutExpo = 1 - Math.pow(2, -10 * progress)
const currentCount = start + (end - start) * easeOutExpo
countRef.current = currentCount
setCount(currentCount)
if (progress < 1) {
animationFrame = requestAnimationFrame(animate)
} else {
// Ensure we end exactly at the target number
countRef.current = end
setCount(end)
}
}
animationFrame = requestAnimationFrame(animate)
}, delay)
return () => {
if (timeout) clearTimeout(timeout)
if (animationFrame) cancelAnimationFrame(animationFrame)
}
}, [inView, start, end, duration, delay])
// Format the number with commas and decimals
const formattedCount = () => {
const value = Math.round(count * Math.pow(10, decimals)) / Math.pow(10, decimals)
return (
prefix +
value.toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}) +
suffix
)
}
return (
<span ref={ref} className={cn("tabular-nums", className)}>
{formattedCount()}
</span>
)
}

@ -0,0 +1,99 @@
"use client"
import * as React from "react"
import { useIsMobile } from "@/hooks/use-mobile"
type SidebarContext = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContext | null>(null)
export function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
export function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}) {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
},
[setOpenProp, open],
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
)
return (
<SidebarContext.Provider value={contextValue}>
<div
style={
{
"--sidebar-width": "16rem",
"--sidebar-width-icon": "3rem",
...style,
} as React.CSSProperties
}
className="group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar"
{...props}
>
{children}
</div>
</SidebarContext.Provider>
)
}

@ -0,0 +1,11 @@
'use client'
import * as React from 'react'
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from 'next-themes'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

@ -0,0 +1,25 @@
"use client"
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(({ id, title, description, action, ...props }) => (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
))}
<ToastViewport />
</ToastProvider>
)
}

@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

@ -0,0 +1,7 @@
"use client"
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }

@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

@ -0,0 +1,262 @@
"use client"
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

@ -0,0 +1,365 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

@ -0,0 +1,153 @@
"use client"
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

@ -0,0 +1,118 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

@ -0,0 +1,178 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

@ -0,0 +1,71 @@
"use client"
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { Dot } from "lucide-react"
import { cn } from "@/lib/utils"
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
))
InputOTP.displayName = "InputOTP"
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
))
InputOTPGroup.displayName = "InputOTPGroup"
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
)
})
InputOTPSlot.displayName = "InputOTPSlot"
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
))
InputOTPSeparator.displayName = "InputOTPSeparator"
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

@ -0,0 +1,236 @@
"use client"
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const MenubarMenu = MenubarPrimitive.Menu
const MenubarGroup = MenubarPrimitive.Group
const MenubarPortal = MenubarPrimitive.Portal
const MenubarSub = MenubarPrimitive.Sub
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
className
)}
{...props}
/>
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...props}
/>
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
)
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}

@ -0,0 +1,128 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}

@ -0,0 +1,117 @@
import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = {
isActive?: boolean
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
}

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

@ -0,0 +1,44 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

@ -0,0 +1,45 @@
"use client"
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

@ -0,0 +1,140 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

@ -0,0 +1,341 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { type VariantProps, cva } from "class-variance-authority"
import { ChevronRight, PanelLeft } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { Sheet, SheetContent } from "@/components/ui/sheet"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { useSidebar } from "@/components/sidebar-provider"
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}
>(({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }, ref) => {
const { isMobile, state, openMobile, setOpenMobile, toggleSidebar } = useSidebar()
if (collapsible === "none") {
return (
<div
className={cn("flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", className)}
ref={ref}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": "18rem",
} as React.CSSProperties
}
side={side}
>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
ref={ref}
className="group peer hidden md:block text-sidebar-foreground"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
)}
/>
<div
className={cn(
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
{/* Expand indicator and clickable area when sidebar is collapsed */}
{state === "collapsed" && (
<>
{/* Wider clickable area along the entire edge */}
<div
onClick={toggleSidebar}
className={cn(
"absolute inset-y-0 w-6 cursor-pointer transition-colors hover:bg-primary/10",
side === "left" ? "left-full" : "right-full",
)}
aria-label="Expand sidebar"
/>
{/* Visual indicator */}
<div
onClick={toggleSidebar}
className={cn(
"absolute flex h-24 w-6 items-center justify-center rounded-r-md bg-primary/10 opacity-0 shadow-sm transition-opacity duration-200 hover:opacity-100 focus:opacity-100 group-hover:opacity-80",
side === "left" ? "left-full top-20" : "right-full top-20",
)}
>
<ChevronRight className={cn("h-4 w-4 text-primary", side === "right" && "rotate-180")} />
</div>
</>
)}
</div>
</div>
)
})
Sidebar.displayName = "Sidebar"
const SidebarTrigger = React.forwardRef<React.ElementRef<typeof Button>, React.ComponentProps<typeof Button>>(
({ className, onClick, ...props }, ref) => {
const { toggleSidebar, state } = useSidebar()
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn(
"h-7 w-7 transition-transform duration-200",
state === "collapsed" ? "rotate-180" : "",
className,
)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
},
)
SidebarTrigger.displayName = "SidebarTrigger"
const SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return <div ref={ref} data-sidebar="header" className={cn("flex flex-col gap-2 p-2", className)} {...props} />
})
SidebarHeader.displayName = "SidebarHeader"
const SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return <div ref={ref} data-sidebar="footer" className={cn("flex flex-col gap-2 p-2", className)} {...props} />
})
SidebarFooter.displayName = "SidebarFooter"
const SidebarSeparator = React.forwardRef<React.ElementRef<typeof Separator>, React.ComponentProps<typeof Separator>>(
({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
)
},
)
SidebarSeparator.displayName = "SidebarSeparator"
const SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
/>
)
})
SidebarContent.displayName = "SidebarContent"
const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
})
SidebarGroup.displayName = "SidebarGroup"
const SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.ComponentProps<"div"> & { asChild?: boolean }>(
({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div"
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
{...props}
/>
)
},
)
SidebarGroupLabel.displayName = "SidebarGroupLabel"
const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div ref={ref} data-sidebar="group-content" className={cn("w-full text-sm", className)} {...props} />
),
)
SidebarGroupContent.displayName = "SidebarGroupContent"
const SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(({ className, ...props }, ref) => (
<ul ref={ref} data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-1", className)} {...props} />
))
SidebarMenu.displayName = "SidebarMenu"
const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
<li ref={ref} data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props} />
))
SidebarMenuItem.displayName = "SidebarMenuItem"
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>
>(({ asChild = false, isActive = false, variant = "default", size = "default", tooltip, className, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile} {...tooltip} />
</Tooltip>
</TooltipProvider>
)
})
SidebarMenuButton.displayName = "SidebarMenuButton"
const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<"main">>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className,
)}
{...props}
/>
)
})
SidebarInset.displayName = "SidebarInset"
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarSeparator,
SidebarTrigger,
}

@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

@ -0,0 +1,31 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

@ -0,0 +1,117 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

@ -0,0 +1,112 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className,
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

@ -0,0 +1,35 @@
"use client"
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
))
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }

@ -0,0 +1,45 @@
"use client"
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3 min-w-10",
sm: "h-9 px-2.5 min-w-9",
lg: "h-11 px-5 min-w-11",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

Some files were not shown because too many files have changed in this diff Show More