Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
66f170a21d | ||
![]() |
227740a13f | ||
![]() |
2ac5bd1b89 |
.gitignorego.modgo.sum
discovery
initial
ENHANCE_INTEGRATE_ADOPT.mdHIGH_LEVEL_CORE_MVP.mdINITIAL_CONCEPT.mdPLAN.mdPRE-GUIDANCE_SNIPPETS.mdSERVICE_SEGREGATION_RESPONSIBILITIES.mdSTACK_GUIDANCE.md
refined
poc
backend
contract
frontend
ui/prototype
.gitignorecomponents.json
app
about
client-layout.tsxcontributions
globals.cssinsights
issues
layout.tsxpage.tsxprofile
resolutions
submit
components
app-sidebar.tsxbreadcrumbs.tsxconnect-wallet-button.tsxcount-up-animation.tsxsidebar-provider.tsxtheme-provider.tsxtoaster.tsx
ui
accordion.tsxalert-dialog.tsxalert.tsxaspect-ratio.tsxavatar.tsxbadge.tsxbreadcrumb.tsxbutton.tsxcalendar.tsxcard.tsxcarousel.tsxchart.tsxcheckbox.tsxcollapsible.tsxcommand.tsxcontext-menu.tsxdialog.tsxdrawer.tsxdropdown-menu.tsxform.tsxhover-card.tsxinput-otp.tsxinput.tsxlabel.tsxmenubar.tsxnavigation-menu.tsxpagination.tsxpopover.tsxprogress.tsxradio-group.tsxresizable.tsxscroll-area.tsxselect.tsxseparator.tsxsheet.tsxsidebar.tsxskeleton.tsxslider.tsxsonner.tsxswitch.tsxtable.tsxtabs.tsxtextarea.tsxtoast.tsxtoaster.tsxtoggle-group.tsxtoggle.tsxtooltip.tsx
6
.gitignore
vendored
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 system’s architecture is modular, allowing each component to be developed
|
||||
### 5. Frontend Layer
|
||||
- **Purpose**: Offers a user-friendly interface for feedback submission, editing, and tracking.
|
||||
- **Technology**:
|
||||
- Elm for a secure, functional UI.
|
||||
- Next.js for a secure, functional UI.
|
||||
- JavaScript for cryptographic tasks (e.g., ZKP generation).
|
||||
- **Key Features**:
|
||||
- Accessible design (WCAG-compliant).
|
||||
@ -68,7 +68,7 @@ We’ll use a phased, iterative approach to build the system, ensuring each comp
|
||||
1. Deploy Hyperledger Fabric network.
|
||||
2. Implement chaincode for feedback submission with simulated ZKPs (e.g., token-based).
|
||||
3. Build an AI microservice for feedback categorization and text generation.
|
||||
4. Create a basic Elm frontend for feedback submission.
|
||||
4. Create a basic Next.js frontend for feedback submission.
|
||||
- **Success Metrics**:
|
||||
- Blockchain stores test feedback.
|
||||
- AI accurately categorizes and generates text from sample data.
|
@ -51,9 +51,9 @@ Your system can be broken down into six core services, each with distinct respon
|
||||
- Provide a user-friendly interface for feedback submission, editing, and tracking.
|
||||
- Handle client-side cryptographic operations (e.g., ZKP generation).
|
||||
- **Languages**:
|
||||
- **Elm**: For the core UI logic (secure, functional, and maintainable).
|
||||
- **Next.js**: For the core UI logic (secure, functional, and maintainable).
|
||||
- **JavaScript**: For cryptographic tasks (e.g., ZKP generation via `snarkjs`).
|
||||
- **Why Elm and JavaScript?**: Elm ensures a robust, type-safe UI, while JavaScript handles browser-based cryptography.
|
||||
- **Why Next.js and JavaScript?**: Next.js ensures a robust, type-safe UI, while JavaScript handles browser-based cryptography.
|
||||
- **Integration**:
|
||||
- Communicates with backend services via gRPC-Web or a proxy.
|
||||
- Uses WebSockets or polling for real-time updates (e.g., live legislative tracking).
|
||||
@ -81,7 +81,7 @@ legislative-platform/
|
||||
├── ai-service/ # Python: AI analysis and text generation
|
||||
├── zkp-service/ # Go: ZKP verification
|
||||
├── collaboration-service/ # Go: Git-like collaboration logic
|
||||
├── frontend-service/ # Elm + JS: User interface and client-side crypto
|
||||
├── frontend-service/ # Next.js + JS: User interface and client-side crypto
|
||||
├── integration-service/ # Go: External system integration
|
||||
├── shared/ # Common libraries, protos, etc.
|
||||
├── docs/ # Documentation
|
@ -8,13 +8,13 @@
|
||||
- **Why**: While you’re more comfortable in Go, Python offers mature libraries for advanced cryptography, like blind signatures (e.g., `blind-signatures` package), which are less developed in Go. Python’s ecosystem also supports prototyping complex cryptographic components quickly.
|
||||
- **Use Case**: Implement cryptographic primitives like blind signatures in Python, then expose them to your Go services via gRPC.
|
||||
|
||||
### 3. **JavaScript and Elm for the Frontend**
|
||||
- **Why**: Your experience with JavaScript, combined with your colleague’s Elm expertise, makes this a strong frontend combo. Elm’s type safety and functional nature ensure a secure, reliable user interface, while JavaScript can handle client-side cryptographic tasks via interop.
|
||||
- **Use Case**: Use Elm to build the UI, guiding users through processes like submitting data or generating tokens. Leverage JavaScript libraries (e.g., `crypto-js`) for client-side cryptography when needed.
|
||||
### 3. **JavaScript and Next.js for the Frontend**
|
||||
- **Why**: Your experience with JavaScript, combined with your colleague’s Next.js expertise, makes this a strong frontend combo. Typescript’s type safety and functional nature ensure a secure, reliable user interface, while JavaScript can handle client-side cryptographic tasks via interop.
|
||||
- **Use Case**: Use Next.js to build the UI, guiding users through processes like submitting data or generating tokens. Leverage JavaScript libraries (e.g., `crypto-js`) for client-side cryptography when needed.
|
||||
|
||||
### 4. **Protocol Buffers and gRPC**
|
||||
- **Why**: Your preference for Protocol Buffers and gRPC aligns perfectly with building secure, efficient communication between services. Go has excellent gRPC support, and Connect-wrapped gRPC adds HTTP/1.1 compatibility for broader client access.
|
||||
- **Use Case**: Define service interfaces with Protocol Buffers and use gRPC for communication between your Go backend, Python cryptographic services, and Elm frontend (via JavaScript interop).
|
||||
- **Use Case**: Define service interfaces with Protocol Buffers and use gRPC for communication between your Go backend, Python cryptographic services, and Next.js frontend (via JavaScript interop).
|
||||
|
||||
---
|
||||
|
||||
@ -45,10 +45,10 @@
|
||||
- **What**: Transition to a blockchain like Ethereum for decentralization.
|
||||
- **How**: Write smart contracts in Solidity and use `go-ethereum` in your Go services to interact with the chain.
|
||||
|
||||
### **4. Frontend with Elm**
|
||||
- **What**: Build a secure, user-friendly interface in Elm, leveraging your colleague’s expertise.
|
||||
### **4. Frontend with Next.js**
|
||||
- **What**: Build a secure, user-friendly interface in Next.js, leveraging your colleague’s expertise.
|
||||
- **How**:
|
||||
- Handle user interactions (e.g., submitting opinions, generating tokens) in Elm.
|
||||
- Handle user interactions (e.g., submitting opinions, generating tokens) in Next.js.
|
||||
- For cryptographic operations (e.g., commitments), use JavaScript interop to call libraries like `crypto-js` or `sjcl`.
|
||||
|
||||
### **5. Communication with gRPC**
|
||||
@ -56,13 +56,13 @@
|
||||
- **How**:
|
||||
- Define your APIs in Protocol Buffers (e.g., `.proto` files).
|
||||
- Implement gRPC servers in Go for the backend and in Python for cryptographic services.
|
||||
- Use gRPC clients in Elm (via JavaScript interop) to connect to the backend.
|
||||
- Use gRPC clients in Next.js (via JavaScript interop) to connect to the backend.
|
||||
|
||||
---
|
||||
|
||||
## Why This Approach Works for You
|
||||
- **Plays to Your Strengths**: Go is your core skill, so it handles the heavy lifting. Python and JavaScript fill gaps where Go’s ecosystem is less mature, using languages you’ve worked with before.
|
||||
- **Leverages Your Team**: Your Elm developer can own the frontend, ensuring a high-quality UI while you focus on the backend and integration.
|
||||
- **Leverages Your Team**: Your Next.js developer can own the frontend, ensuring a high-quality UI while you focus on the backend and integration.
|
||||
- **Matches Your Preferences**: Protocol Buffers and gRPC are central to the architecture, providing the efficient, secure communication you enjoy working with.
|
||||
- **Scalable Design**: Start simple with a centralized database, then scale to a blockchain if needed, all while keeping your codebase modular and maintainable.
|
||||
|
||||
@ -71,11 +71,11 @@
|
||||
## Next Steps to Get Started
|
||||
1. **Go Backend**: Set up a basic gRPC server in Go using Protocol Buffers. Implement a simple hashing function with `crypto/sha256` to test commitments.
|
||||
2. **Python Cryptography**: Prototype blind signatures in Python with `blind-signatures` and expose them via a gRPC service.
|
||||
3. **Elm Frontend**: Work with your Elm developer to create a basic UI that sends requests to your Go backend via gRPC (using JS interop).
|
||||
3. **Next.js Frontend**: Work with your Next.js developer to create a basic UI that sends requests to your Go backend via gRPC (using JS interop).
|
||||
4. **ZKP Exploration**: Experiment with Circom and `snarkjs` to build a small ZKP circuit, then verify it in Go with `gnark`.
|
||||
5. **Ledger Setup**: Start with PostgreSQL in Go, storing hashed data, and plan for a blockchain pivot later if required.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
This approach lets you use Go for the core, Python for cryptography where Go lacks, Elm for a secure frontend, and gRPC for communication—all aligned with your skills and preferences. You’ll build a solid system that’s secure, efficient, and extensible, with room to grow into more advanced features like blockchain integration.
|
||||
This approach lets you use Go for the core, Python for cryptography where Go lacks, Next.js for a secure frontend, and gRPC for communication—all aligned with your skills and preferences. You’ll build a solid system that’s secure, efficient, and extensible, with room to grow into more advanced features like blockchain integration.
|
167
discovery/refined/DECENTRALIZATION.md
Normal file
167
discovery/refined/DECENTRALIZATION.md
Normal file
@ -0,0 +1,167 @@
|
||||
### Embracing Full Decentralization: Impact on System Design
|
||||
|
||||
Your commitment to full decentralization is a brilliant shift—it aligns with blockchain’s ethos and makes your system resilient, uncensorable, and community-driven. Stepping outside your comfort zone is a worthwhile trade-off for this level of robustness. Here’s how it changes your system design, languages, architecture, and development approach:
|
||||
|
||||
#### 1. **Decentralized Frontend Hosting**
|
||||
- **Change**: Host your frontend (e.g., Next.js app) on **IPFS** or **Arweave** instead of a traditional server.
|
||||
- **Impact**: Users access the app via decentralized gateways (e.g., `ipfs.io`), eliminating reliance on centralized hosting that could be shut down.
|
||||
- **Tech**: Use tools like **Fleek** or **Pinata** to deploy and pin your frontend on IPFS.
|
||||
- **Comfort Zone Shift**: You’ll need to learn IPFS deployment, but these tools provide straightforward workflows.
|
||||
|
||||
#### 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 ID’s ZKP verification to your smart contracts.
|
||||
- **Comfort Zone Shift**: Requires learning ZKP integration, but Privado ID’s 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**: You’ll 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**: You’ll 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 app’s 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 it’s a simple process.
|
||||
|
||||
---
|
||||
|
||||
### Changes to Languages and Architecture
|
||||
|
||||
Here’s how decentralization reshapes your tech stack:
|
||||
|
||||
- **Frontend**:
|
||||
- **Language**: Next.js remains, but you’ll add wallet integration (e.g., MetaMask) and IPFS gateway logic.
|
||||
- **Architecture**: Hosted on IPFS, interacting directly with smart contracts.
|
||||
- **Backend**:
|
||||
- **Language**: Go or TypeScript, but minimized—most logic shifts to smart contracts or decentralized services.
|
||||
- **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, I’ll 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. Let’s dive into the details.
|
||||
|
||||
---
|
||||
|
||||
### Where Is the Backend Deployed?
|
||||
|
||||
In a fully decentralized system, the backend isn’t hosted on a single server or cloud provider like AWS. Instead, it’s distributed across decentralized infrastructure to ensure no central authority controls it. Here’s how and where it’s 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. They’re 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.
|
||||
- **Fleek’s 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 can’t 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 they’re handled by decentralized components rather than a central server. Here’s 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 can’t 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 user’s eligibility to participate without a central login server.
|
||||
|
||||
---
|
||||
|
||||
### How This Differs from a Traditional Backend
|
||||
|
||||
To clarify, here’s a quick comparison:
|
||||
|
||||
| **Responsibility** | **Traditional Backend** | **Decentralized Backend** |
|
||||
|---------------------------|---------------------------------|-----------------------------------------|
|
||||
| **Data Storage** | Central database | Blockchain (key data), IPFS (bulk data) |
|
||||
| **Business Logic** | Server-side code | Smart contracts |
|
||||
| **Heavy Computation** | Server processing | Decentralized compute platforms |
|
||||
| **Authentication** | Centralized login (e.g., OAuth) | Decentralized identity (e.g., SSI) |
|
||||
| **Communication** | WebSockets on servers | P2P protocols (e.g., libp2p) |
|
||||
|
||||
In a decentralized system, the backend’s role is minimized—smart contracts handle most logic, and other tasks are offloaded to decentralized services or user devices.
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
42
discovery/refined/GLOSSARY.md
Normal file
42
discovery/refined/GLOSSARY.md
Normal file
@ -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 user’s 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 community’s 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 project’s 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 public’s 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.
|
186
discovery/refined/IMPLEMENTATION_PLAN.md
Normal file
186
discovery/refined/IMPLEMENTATION_PLAN.md
Normal file
@ -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 ID’s cross-chain features.
|
||||
|
||||
---
|
||||
|
||||
Here’s 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. It’s 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 ID’s 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
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. Here’s 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 user’s address and an issue identifier, is recorded on the Polygon Proof-of-Stake (PoS) blockchain.
|
||||
- **Example**: A citizen submits a Perspective via the platform’s frontend. The system stores the text on IPFS and logs the hash on-chain, ensuring it’s 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 platform’s 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**
|
||||
Discourse’s 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., "I’m 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, Discourse’s 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 can’t 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 user’s 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 blockchain’s transparency, AI’s intelligence, and decentralized governance, it offers a secure, inclusive, and resilient way to influence legislation. Whether you’re 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.
|
107
discovery/refined/SMART_CONTRACT.md
Normal file
107
discovery/refined/SMART_CONTRACT.md
Normal file
@ -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. Here’s 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 submitter’s 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 Insight’s 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 Insight’s 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
|
||||
Here’s 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; // Submitter’s address
|
||||
}
|
||||
|
||||
struct Insight {
|
||||
string description; // Insight text
|
||||
uint256 perspectiveCount; // Number of Perspectives it’s 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
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
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=
|
264
poc/backend/feedback/feedback.go
Normal file
264
poc/backend/feedback/feedback.go
Normal file
@ -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
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
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
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
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
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
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
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
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
|
273
ui/prototype/app/about/page.tsx
Normal file
273
ui/prototype/app/about/page.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
|
50
ui/prototype/app/client-layout.tsx
Normal file
50
ui/prototype/app/client-layout.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
|
3
ui/prototype/app/contributions/[id]/loading.tsx
Normal file
3
ui/prototype/app/contributions/[id]/loading.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return <div className="container mx-auto p-4">Loading contribution details...</div>
|
||||
}
|
499
ui/prototype/app/contributions/[id]/page.tsx
Normal file
499
ui/prototype/app/contributions/[id]/page.tsx
Normal file
@ -0,0 +1,499 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
import { use } from "react"
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
ExternalLink,
|
||||
MessageCircle,
|
||||
ThumbsUp,
|
||||
Users,
|
||||
FileText,
|
||||
CalendarDays,
|
||||
LineChart,
|
||||
AlertCircle
|
||||
} from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger
|
||||
} from "@/components/ui/tabs"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
|
||||
// Mock data for a single contribution
|
||||
const mockContributions = [
|
||||
{
|
||||
id: 1,
|
||||
title: "After-School Programs",
|
||||
description: "Create free after-school programs for K-8 students in public schools",
|
||||
category: "Education",
|
||||
votes: { yes: 312, no: 28 },
|
||||
perspectives: 42,
|
||||
dateCreated: "2023-10-05",
|
||||
dateValidated: "2023-11-15",
|
||||
status: "Validated",
|
||||
resolutionId: 12,
|
||||
resolutionTitle: "Education and Community Health Improvements",
|
||||
metrics: {
|
||||
sentimentScore: 0.87,
|
||||
participationRate: 0.62,
|
||||
consensusSpeed: "Fast",
|
||||
implementationComplexity: "Medium",
|
||||
},
|
||||
sourceInsight: {
|
||||
id: 3,
|
||||
title: "After-School Programs",
|
||||
description: "Create free after-school programs for K-8 students in public schools"
|
||||
},
|
||||
relatedPerspectives: [
|
||||
{
|
||||
id: 101,
|
||||
username: "parent87",
|
||||
content: "As a working parent, having free after-school programs would be life-changing for me and my children.",
|
||||
date: "2023-09-15",
|
||||
votes: 43,
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
username: "educator22",
|
||||
content: "After-school programs provide crucial educational support and keep children engaged in positive activities.",
|
||||
date: "2023-09-18",
|
||||
votes: 38,
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
username: "communityLeader",
|
||||
content: "These programs would reduce juvenile crime rates in neighborhoods where working parents can't be home until evening.",
|
||||
date: "2023-09-22",
|
||||
votes: 29,
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
username: "budgetAnalyst",
|
||||
content: "The ROI is excellent - the cost of these programs is lower than dealing with the social issues that arise without them.",
|
||||
date: "2023-09-25",
|
||||
votes: 36,
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
username: "socialWorker42",
|
||||
content: "Many families in underserved areas would greatly benefit from structured after-school care and enrichment.",
|
||||
date: "2023-09-28",
|
||||
votes: 41,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Community Health Clinics",
|
||||
description: "Establish walk-in clinics in underserved areas with sliding scale fees",
|
||||
category: "Healthcare",
|
||||
votes: { yes: 278, no: 42 },
|
||||
perspectives: 36,
|
||||
dateCreated: "2023-11-10",
|
||||
dateValidated: "2023-12-03",
|
||||
status: "Validated",
|
||||
resolutionId: 12,
|
||||
resolutionTitle: "Education and Community Health Improvements",
|
||||
metrics: {
|
||||
sentimentScore: 0.82,
|
||||
participationRate: 0.58,
|
||||
consensusSpeed: "Medium",
|
||||
implementationComplexity: "High",
|
||||
},
|
||||
sourceInsight: {
|
||||
id: 4,
|
||||
title: "Community Health Clinics",
|
||||
description: "Establish walk-in clinics in underserved areas with sliding scale fees"
|
||||
},
|
||||
relatedPerspectives: [
|
||||
// Perspectives data would go here
|
||||
]
|
||||
},
|
||||
// Other contributions would go here
|
||||
]
|
||||
|
||||
export default function ContributionPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
// Properly handle the params with React.use()
|
||||
const { id } = use(params)
|
||||
const contributionId = parseInt(id)
|
||||
const [activeTab, setActiveTab] = useState("overview")
|
||||
|
||||
// Find the contribution from mock data
|
||||
const contribution = mockContributions.find(c => c.id === contributionId)
|
||||
|
||||
if (!contribution) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-16 text-center">
|
||||
<AlertCircle className="mx-auto h-16 w-16 text-muted-foreground mb-4" />
|
||||
<h1 className="text-2xl font-bold mb-2">Contribution Not Found</h1>
|
||||
<p className="text-muted-foreground mb-8">The contribution you're looking for doesn't exist or has been removed.</p>
|
||||
<Button asChild>
|
||||
<Link href="/contributions">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Return to Contributions
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Approval percentage
|
||||
const approvalPercentage = Math.round((contribution.votes.yes / (contribution.votes.yes + contribution.votes.no)) * 100)
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<Button variant="outline" size="sm" className="mb-4" asChild>
|
||||
<Link href="/contributions">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Contributions
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Badge className="text-xs">{contribution.category}</Badge>
|
||||
<Badge variant="outline" className="text-xs">ID: {contribution.id}</Badge>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold flex items-center gap-2">
|
||||
<CheckCircle className="h-6 w-6 text-primary" />
|
||||
{contribution.title}
|
||||
</h1>
|
||||
<p className="mt-2 text-muted-foreground">{contribution.description}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-start md:items-end">
|
||||
<Badge variant="secondary" className="mb-2">
|
||||
{contribution.status}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<CalendarDays className="h-4 w-4 mr-1 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Validated: </span>
|
||||
<span>{new Date(contribution.dateValidated).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<Button size="sm" asChild>
|
||||
<Link href={`/resolutions/${contribution.resolutionId}`}>
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
View in Resolution #{contribution.resolutionId}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="overview" value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="perspectives">Source Perspectives</TabsTrigger>
|
||||
<TabsTrigger value="metrics">Metrics & Analysis</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Overview Tab */}
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Consensus Details</CardTitle>
|
||||
<CardDescription>How this contribution reached consensus</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex justify-between mb-2 text-sm">
|
||||
<span>Approval: {approvalPercentage}%</span>
|
||||
<span>
|
||||
<span className="text-green-600">{contribution.votes.yes} Yes</span> / <span className="text-red-600">{contribution.votes.no} No</span>
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={approvalPercentage} className="h-2" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Based On</p>
|
||||
<p className="text-2xl font-bold">{contribution.perspectives}</p>
|
||||
<p className="text-xs text-muted-foreground">unique perspectives</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Consensus Speed</p>
|
||||
<p className="text-2xl font-bold">{contribution.metrics.consensusSpeed}</p>
|
||||
<p className="text-xs text-muted-foreground">relative to average</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Timeline</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="h-6 w-6 rounded-full bg-primary flex items-center justify-center text-primary-foreground">
|
||||
<MessageCircle className="h-3 w-3" />
|
||||
</div>
|
||||
<div className="w-px h-full bg-muted-foreground/30 my-1"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Perspectives Collection</p>
|
||||
<p className="text-xs text-muted-foreground">Started: {new Date('2023-09-01').toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="h-6 w-6 rounded-full bg-amber-500 flex items-center justify-center text-white">
|
||||
<LineChart className="h-3 w-3" />
|
||||
</div>
|
||||
<div className="w-px h-full bg-muted-foreground/30 my-1"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Insight Generated</p>
|
||||
<p className="text-xs text-muted-foreground">Date: {new Date(contribution.dateCreated).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="h-6 w-6 rounded-full bg-green-500 flex items-center justify-center text-white">
|
||||
<ThumbsUp className="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Consensus Reached</p>
|
||||
<p className="text-xs text-muted-foreground">Date: {new Date(contribution.dateValidated).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Resolution Integration</CardTitle>
|
||||
<CardDescription>How this contribution impacts policy</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium mb-2">Included in Resolution</p>
|
||||
<div className="flex items-center gap-2 p-3 border rounded-md">
|
||||
<FileText className="h-5 w-5 text-blue-500" />
|
||||
<div>
|
||||
<p className="font-medium">Resolution #{contribution.resolutionId}</p>
|
||||
<p className="text-sm text-muted-foreground">{contribution.resolutionTitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Implementation Complexity</h4>
|
||||
<Badge variant={contribution.metrics.implementationComplexity === "Low" ? "outline" :
|
||||
contribution.metrics.implementationComplexity === "Medium" ? "secondary" :
|
||||
"destructive"}>
|
||||
{contribution.metrics.implementationComplexity}
|
||||
</Badge>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
This contribution has been assessed as having {contribution.metrics.implementationComplexity.toLowerCase()}
|
||||
implementation complexity based on required resources, timeline, and regulatory considerations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Source Insight</h4>
|
||||
<Link href={`/insights/${contribution.sourceInsight.id}`} className="block p-3 border rounded-md hover:bg-accent transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">{contribution.sourceInsight.title}</p>
|
||||
<p className="text-sm text-muted-foreground truncate">{contribution.sourceInsight.description}</p>
|
||||
</div>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Perspectives Tab */}
|
||||
<TabsContent value="perspectives" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Source Perspectives</CardTitle>
|
||||
<CardDescription>Original community input that led to this contribution</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{contribution.relatedPerspectives.map((perspective) => (
|
||||
<div key={perspective.id} className="p-4 border rounded-lg">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={`https://api.dicebear.com/7.x/initials/svg?seed=${perspective.username}`} alt={perspective.username} />
|
||||
<AvatarFallback>{perspective.username.substring(0, 2).toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium">{perspective.username}</p>
|
||||
<p className="text-xs text-muted-foreground">{new Date(perspective.date).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1">
|
||||
<ThumbsUp className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm">{perspective.votes}</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Community upvotes</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<p className="text-sm">{perspective.content}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Metrics Tab */}
|
||||
<TabsContent value="metrics" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Metrics & Analysis</CardTitle>
|
||||
<CardDescription>Statistical breakdown of community engagement</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Sentiment Analysis</h4>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-16 w-16 rounded-full border-4 border-green-500 flex items-center justify-center">
|
||||
<span className="text-xl font-bold">{contribution.metrics.sentimentScore * 10}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Positive Sentiment</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{contribution.metrics.sentimentScore > 0.8 ? "Very Positive" :
|
||||
contribution.metrics.sentimentScore > 0.6 ? "Positive" :
|
||||
contribution.metrics.sentimentScore > 0.4 ? "Neutral" : "Mixed"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
AI analysis of perspective text shows strong support for this contribution.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Demographic Distribution</h4>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="flex justify-between mb-1 text-xs">
|
||||
<span>Age Groups</span>
|
||||
</div>
|
||||
<div className="flex h-2">
|
||||
<div className="h-full bg-blue-300" style={{ width: "15%" }}></div>
|
||||
<div className="h-full bg-blue-400" style={{ width: "25%" }}></div>
|
||||
<div className="h-full bg-blue-500" style={{ width: "35%" }}></div>
|
||||
<div className="h-full bg-blue-600" style={{ width: "20%" }}></div>
|
||||
<div className="h-full bg-blue-700" style={{ width: "5%" }}></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs mt-1">
|
||||
<span>18-24</span>
|
||||
<span>25-34</span>
|
||||
<span>35-44</span>
|
||||
<span>45-64</span>
|
||||
<span>65+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Participation Rate</h4>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative h-16 w-16">
|
||||
<svg className="h-full w-full" viewBox="0 0 36 36">
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="#eee"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
<path
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth="3"
|
||||
strokeDasharray={`${contribution.metrics.participationRate * 100}, 100`}
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-sm font-semibold">
|
||||
{Math.round(contribution.metrics.participationRate * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Community Engagement</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{contribution.metrics.participationRate > 0.7 ? "Exceptional" :
|
||||
contribution.metrics.participationRate > 0.5 ? "High" :
|
||||
contribution.metrics.participationRate > 0.3 ? "Moderate" : "Low"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Geographic Impact</h4>
|
||||
<div className="p-3 border rounded-md">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Users className="h-5 w-5 text-primary" />
|
||||
<p className="font-medium">City-wide Relevance</p>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
This contribution has been flagged as having city-wide relevance with particular
|
||||
significance for underserved communities.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant="outline">Downtown</Badge>
|
||||
<Badge variant="outline">South Side</Badge>
|
||||
<Badge variant="outline">West End</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12 flex justify-center">
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/contributions">Return to All Contributions</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
4
ui/prototype/app/contributions/loading.tsx
Normal file
4
ui/prototype/app/contributions/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
|
235
ui/prototype/app/contributions/page.tsx
Normal file
235
ui/prototype/app/contributions/page.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
|
80
ui/prototype/app/globals.css
Normal file
80
ui/prototype/app/globals.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
|
4
ui/prototype/app/insights/loading.tsx
Normal file
4
ui/prototype/app/insights/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
|
810
ui/prototype/app/insights/page.tsx
Normal file
810
ui/prototype/app/insights/page.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
|
460
ui/prototype/app/issues/[id]/page.tsx
Normal file
460
ui/prototype/app/issues/[id]/page.tsx
Normal file
@ -0,0 +1,460 @@
|
||||
"use client"
|
||||
|
||||
import { useState, use } from "react"
|
||||
import { ArrowLeft, MessageSquare, ThumbsUp, ThumbsDown, Share2, FileText, Filter } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
|
||||
// Mock data for the issue
|
||||
const mockIssues = {
|
||||
"climate-action": {
|
||||
id: "climate-action",
|
||||
title: "Climate Action Policies",
|
||||
description: "Discussion on policies to address climate change and reduce carbon emissions.",
|
||||
category: "Environmental Policy",
|
||||
dateCreated: "2024-01-10",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 324,
|
||||
insights: 18,
|
||||
contributions: 5,
|
||||
participants: 156
|
||||
}
|
||||
},
|
||||
"education-reform": {
|
||||
id: "education-reform",
|
||||
title: "Education System Reform",
|
||||
description: "Ideas for improving educational outcomes and accessibility for all students.",
|
||||
category: "Education",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 187,
|
||||
insights: 12,
|
||||
contributions: 3,
|
||||
participants: 89
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock perspectives for issues
|
||||
const mockPerspectivesByIssue = {
|
||||
"climate-action": [
|
||||
{
|
||||
id: 1,
|
||||
userId: "user1",
|
||||
userName: "Alex Johnson",
|
||||
userAvatar: "",
|
||||
content: "We should focus on renewable energy investments. Solar and wind power have become more cost-effective and could replace fossil fuels in many regions.",
|
||||
dateSubmitted: "2024-03-01",
|
||||
likes: 45,
|
||||
dislikes: 3
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userId: "user2",
|
||||
userName: "Jamie Smith",
|
||||
userAvatar: "",
|
||||
content: "Carbon pricing is the most efficient way to reduce emissions. It creates market incentives for businesses to innovate and cut their carbon footprint.",
|
||||
dateSubmitted: "2024-02-28",
|
||||
likes: 38,
|
||||
dislikes: 7
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
userId: "user3",
|
||||
userName: "Taylor Reed",
|
||||
userAvatar: "",
|
||||
content: "We need to address transportation emissions through better public transit and EV infrastructure. This sector is a major contributor to greenhouse gases.",
|
||||
dateSubmitted: "2024-02-25",
|
||||
likes: 52,
|
||||
dislikes: 4
|
||||
}
|
||||
],
|
||||
"education-reform": [
|
||||
{
|
||||
id: 1,
|
||||
userId: "user4",
|
||||
userName: "Morgan Lee",
|
||||
userAvatar: "",
|
||||
content: "Teachers need better resources and smaller class sizes to effectively improve student outcomes.",
|
||||
dateSubmitted: "2024-03-02",
|
||||
likes: 62,
|
||||
dislikes: 5
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userId: "user5",
|
||||
userName: "Casey Wilson",
|
||||
userAvatar: "",
|
||||
content: "Access to early childhood education should be a priority as it sets the foundation for all future learning.",
|
||||
dateSubmitted: "2024-02-27",
|
||||
likes: 41,
|
||||
dislikes: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Mock insights for issues
|
||||
const mockInsightsByIssue = {
|
||||
"climate-action": [
|
||||
{
|
||||
id: 1,
|
||||
title: "Renewable Energy Investment",
|
||||
description: "67% of perspectives support increased government funding for renewable energy projects, with solar and wind being the most frequently mentioned technologies.",
|
||||
votes: { yes: 124, no: 18 },
|
||||
status: "consensus", // voting, consensus, rejected
|
||||
perspectives: [1, 5, 8, 12] // Reference to perspective IDs
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Carbon Pricing Mechanism",
|
||||
description: "A majority of users advocate for carbon pricing policies, with 58% specifically mentioning tax incentives for low-emission businesses.",
|
||||
votes: { yes: 98, no: 32 },
|
||||
status: "voting", // voting, consensus, rejected
|
||||
perspectives: [2, 7, 15] // Reference to perspective IDs
|
||||
}
|
||||
],
|
||||
"education-reform": [
|
||||
{
|
||||
id: 1,
|
||||
title: "Teacher Support Systems",
|
||||
description: "78% of perspectives emphasize the need for better resources and support for teachers, including smaller class sizes and professional development.",
|
||||
votes: { yes: 112, no: 15 },
|
||||
status: "consensus",
|
||||
perspectives: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Early Childhood Education Access",
|
||||
description: "64% of perspectives advocate for universal access to early childhood education to establish a strong foundation for learning.",
|
||||
votes: { yes: 87, no: 34 },
|
||||
status: "voting",
|
||||
perspectives: [2, 8, 10]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default function IssueDetails({ params }: { params: Promise<{ id: string }> }) {
|
||||
// Properly unwrap params using React.use()
|
||||
const { id: issueId } = use(params)
|
||||
const [activeTab, setActiveTab] = useState("overview")
|
||||
const [commentText, setCommentText] = useState("")
|
||||
|
||||
// Get the issue data from our mock data
|
||||
const issue = mockIssues[issueId as keyof typeof mockIssues] || mockIssues["climate-action"]
|
||||
const perspectives = mockPerspectivesByIssue[issueId as keyof typeof mockPerspectivesByIssue] || []
|
||||
const insights = mockInsightsByIssue[issueId as keyof typeof mockInsightsByIssue] || []
|
||||
|
||||
const handleSubmitComment = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (commentText.trim()) {
|
||||
// In a real app, submit the comment to the backend
|
||||
console.log("Submitting comment:", commentText)
|
||||
setCommentText("")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-6">
|
||||
<Button variant="ghost" size="sm" asChild className="mb-4">
|
||||
<Link href="/issues">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Issues
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{issue.title}</h1>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Badge variant="outline">{issue.category}</Badge>
|
||||
<Badge variant="secondary" className="capitalize">{issue.status}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-4 md:mt-0">
|
||||
<Button variant="outline" size="sm" className="gap-1">
|
||||
<Share2 className="h-4 w-4" />
|
||||
Share
|
||||
</Button>
|
||||
<Button variant="default" size="sm" asChild>
|
||||
<Link href={`/submit?issue=${issueId}`}>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Add Perspective
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mb-6">{issue.description}</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{issue.metrics.perspectives}</div>
|
||||
<p className="text-sm text-muted-foreground">Perspectives</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{issue.metrics.insights}</div>
|
||||
<p className="text-sm text-muted-foreground">Insights</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{issue.metrics.contributions}</div>
|
||||
<p className="text-sm text-muted-foreground">Contributions</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-2xl font-bold">{issue.metrics.participants}</div>
|
||||
<p className="text-sm text-muted-foreground">Participants</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="perspectives">Perspectives</TabsTrigger>
|
||||
<TabsTrigger value="insights">Insights</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="pt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Issue Summary</CardTitle>
|
||||
<CardDescription>Key information and progress on this issue</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="font-medium mb-2">Progress</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>Perspectives Gathering</span>
|
||||
<span className="text-muted-foreground">75%</span>
|
||||
</div>
|
||||
<Progress value={75} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>Insight Formation</span>
|
||||
<span className="text-muted-foreground">45%</span>
|
||||
</div>
|
||||
<Progress value={45} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>Contribution Validation</span>
|
||||
<span className="text-muted-foreground">20%</span>
|
||||
</div>
|
||||
<Progress value={20} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div>
|
||||
<h3 className="font-medium mb-2">Timeline</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex">
|
||||
<div className="mr-4 mt-0.5">
|
||||
<Badge variant="secondary" className="h-8 w-8 rounded-full flex items-center justify-center p-0">1</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium">Perspectives Collection</h4>
|
||||
<p className="text-sm text-muted-foreground">In progress - Ends Apr 15, 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="mr-4 mt-0.5">
|
||||
<Badge variant="outline" className="h-8 w-8 rounded-full flex items-center justify-center p-0">2</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium">Insight Voting</h4>
|
||||
<p className="text-sm text-muted-foreground">Apr 16 - May 1, 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="mr-4 mt-0.5">
|
||||
<Badge variant="outline" className="h-8 w-8 rounded-full flex items-center justify-center p-0">3</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium">Resolution Formation</h4>
|
||||
<p className="text-sm text-muted-foreground">May 2 - May 15, 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="perspectives" className="pt-6">
|
||||
<div className="flex justify-between mb-4">
|
||||
<h2 className="text-xl font-bold">Recent Perspectives</h2>
|
||||
<div className="flex gap-2">
|
||||
<Select defaultValue="recent">
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="recent">Most Recent</SelectItem>
|
||||
<SelectItem value="popular">Most Popular</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{perspectives.map((perspective) => (
|
||||
<Card key={perspective.id}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Avatar className="h-8 w-8 mr-2">
|
||||
<AvatarImage src={perspective.userAvatar} />
|
||||
<AvatarFallback>{perspective.userName.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-medium">{perspective.userName}</p>
|
||||
<p className="text-xs text-muted-foreground">{perspective.dateSubmitted}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>{perspective.content}</p>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t pt-4 flex justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" className="gap-1">
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
{perspective.likes}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="gap-1">
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
{perspective.dislikes}
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm">Reply</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Add Your Perspective</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmitComment}>
|
||||
<Textarea
|
||||
placeholder="Share your thoughts on this issue..."
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button type="submit" disabled={!commentText.trim()}>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Submit Perspective
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="insights" className="pt-6">
|
||||
<div className="flex justify-between mb-4">
|
||||
<h2 className="text-xl font-bold">Emerging Insights</h2>
|
||||
<Button variant="outline" size="sm">
|
||||
<Filter className="mr-2 h-4 w-4" />
|
||||
Filter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{insights.map((insight) => (
|
||||
<Card key={insight.id}>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between">
|
||||
<CardTitle>{insight.title}</CardTitle>
|
||||
<Badge
|
||||
variant={insight.status === "consensus" ? "default" : "outline"}
|
||||
className={insight.status === "consensus" ? "bg-green-500" : ""}
|
||||
>
|
||||
{insight.status === "consensus" ? "Consensus Reached" : "Voting In Progress"}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4">{insight.description}</p>
|
||||
<div className="rounded-md bg-muted p-4">
|
||||
<div className="text-sm text-muted-foreground mb-2">Voting Progress</div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>
|
||||
<ThumbsUp className="h-3 w-3 inline mr-1" />
|
||||
{insight.votes.yes} Yes
|
||||
</span>
|
||||
<span>
|
||||
<ThumbsDown className="h-3 w-3 inline mr-1" />
|
||||
{insight.votes.no} No
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={(insight.votes.yes / (insight.votes.yes + insight.votes.no)) * 100}
|
||||
className="h-2"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t pt-4 flex justify-between">
|
||||
<div>
|
||||
<Badge variant="outline" className="mr-2">
|
||||
Based on {insight.perspectives.length} perspectives
|
||||
</Badge>
|
||||
</div>
|
||||
{insight.status === "voting" && (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="gap-1">
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
Vote No
|
||||
</Button>
|
||||
<Button variant="default" size="sm" className="gap-1">
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
Vote Yes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
250
ui/prototype/app/issues/page.tsx
Normal file
250
ui/prototype/app/issues/page.tsx
Normal file
@ -0,0 +1,250 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { Search, Filter, ArrowUpDown, Plus } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
|
||||
// Mock data for issues
|
||||
const mockIssues = [
|
||||
{
|
||||
id: "climate-action",
|
||||
title: "Climate Action Policies",
|
||||
description: "Discussion on policies to address climate change and reduce carbon emissions.",
|
||||
category: "Environmental Policy",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 324,
|
||||
insights: 18,
|
||||
contributions: 5
|
||||
},
|
||||
dateCreated: "2024-01-10"
|
||||
},
|
||||
{
|
||||
id: "education-reform",
|
||||
title: "Education System Reform",
|
||||
description: "Ideas for improving educational outcomes and accessibility for all students.",
|
||||
category: "Education",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 187,
|
||||
insights: 12,
|
||||
contributions: 3
|
||||
},
|
||||
dateCreated: "2024-01-22"
|
||||
},
|
||||
{
|
||||
id: "healthcare-access",
|
||||
title: "Healthcare Accessibility",
|
||||
description: "Solutions to make healthcare more affordable and accessible for everyone.",
|
||||
category: "Healthcare",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 256,
|
||||
insights: 15,
|
||||
contributions: 4
|
||||
},
|
||||
dateCreated: "2024-02-05"
|
||||
},
|
||||
{
|
||||
id: "housing-affordability",
|
||||
title: "Housing Affordability Crisis",
|
||||
description: "Addressing the growing housing affordability crisis in urban areas.",
|
||||
category: "Infrastructure",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 210,
|
||||
insights: 8,
|
||||
contributions: 2
|
||||
},
|
||||
dateCreated: "2024-02-15"
|
||||
},
|
||||
{
|
||||
id: "digital-privacy",
|
||||
title: "Digital Privacy Regulations",
|
||||
description: "Balancing innovation with personal privacy in the digital age.",
|
||||
category: "Technology",
|
||||
status: "active",
|
||||
metrics: {
|
||||
perspectives: 156,
|
||||
insights: 7,
|
||||
contributions: 1
|
||||
},
|
||||
dateCreated: "2024-02-28"
|
||||
}
|
||||
]
|
||||
|
||||
// Categories for filtering
|
||||
const categories = [
|
||||
"All Categories",
|
||||
"Environmental Policy",
|
||||
"Education",
|
||||
"Healthcare",
|
||||
"Infrastructure",
|
||||
"Technology",
|
||||
"Economy",
|
||||
"Social Services"
|
||||
]
|
||||
|
||||
export default function IssuesPage() {
|
||||
const [selectedTab, setSelectedTab] = useState("all")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedCategory, setSelectedCategory] = useState("All Categories")
|
||||
const [sortBy, setSortBy] = useState("latest")
|
||||
|
||||
// Filter issues based on search, category, and tab
|
||||
const filteredIssues = mockIssues.filter(issue => {
|
||||
// Search filter
|
||||
const matchesSearch = searchQuery
|
||||
? issue.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
issue.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
: true
|
||||
|
||||
// Category filter
|
||||
const matchesCategory = selectedCategory === "All Categories" || issue.category === selectedCategory
|
||||
|
||||
// Tab filter (all, trending, etc.) - simplified for demo
|
||||
const matchesTab = selectedTab === "all" ||
|
||||
(selectedTab === "trending" && issue.metrics.perspectives > 200)
|
||||
|
||||
return matchesSearch && matchesCategory && matchesTab
|
||||
})
|
||||
|
||||
// Sort issues
|
||||
const sortedIssues = [...filteredIssues].sort((a, b) => {
|
||||
if (sortBy === "latest") {
|
||||
return new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime()
|
||||
} else if (sortBy === "popular") {
|
||||
return b.metrics.perspectives - a.metrics.perspectives
|
||||
} else if (sortBy === "insights") {
|
||||
return b.metrics.insights - a.metrics.insights
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Issues</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Browse and contribute to active policy discussions
|
||||
</p>
|
||||
</div>
|
||||
<Button className="mt-4 sm:mt-0" asChild>
|
||||
<Link href="/issues/propose">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Propose New Issue
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Filters and Search */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search issues..."
|
||||
className="pl-8"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Select
|
||||
value={selectedCategory}
|
||||
onValueChange={setSelectedCategory}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[180px]">
|
||||
<SelectValue placeholder="Category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category) => (
|
||||
<SelectItem key={category} value={category}>
|
||||
{category}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={sortBy}
|
||||
onValueChange={setSortBy}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[180px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="latest">Latest</SelectItem>
|
||||
<SelectItem value="popular">Most Perspectives</SelectItem>
|
||||
<SelectItem value="insights">Most Insights</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon">
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab} className="w-full">
|
||||
<TabsList className="grid w-full sm:w-auto sm:inline-grid grid-cols-3 sm:grid-cols-3">
|
||||
<TabsTrigger value="all">All Issues</TabsTrigger>
|
||||
<TabsTrigger value="trending">Trending</TabsTrigger>
|
||||
<TabsTrigger value="following">Following</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* Results */}
|
||||
{sortedIssues.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<h3 className="text-lg font-medium">No issues found</h3>
|
||||
<p className="text-muted-foreground mt-2">Try adjusting your filters or search terms</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{sortedIssues.map((issue) => (
|
||||
<Link key={issue.id} href={`/issues/${issue.id}`} className="transition-transform hover:scale-[1.01]">
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle className="line-clamp-2">{issue.title}</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
<Badge variant="outline">{issue.category}</Badge>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow">
|
||||
<p className="text-muted-foreground line-clamp-3">{issue.description}</p>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t pt-4">
|
||||
<div className="w-full flex justify-between text-sm text-muted-foreground">
|
||||
<div className="flex gap-4">
|
||||
<div>{issue.metrics.perspectives} Perspectives</div>
|
||||
<div>{issue.metrics.insights} Insights</div>
|
||||
</div>
|
||||
<div>
|
||||
{new Date(issue.dateCreated).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
293
ui/prototype/app/issues/propose/page.tsx
Normal file
293
ui/prototype/app/issues/propose/page.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { ArrowLeft, FileText, HelpCircle, AlertTriangle, CheckCircle, Loader2 } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { useWalletStore } from "@/lib/wallet-store"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
// Categories for selection
|
||||
const categories = [
|
||||
"Environmental Policy",
|
||||
"Education",
|
||||
"Healthcare",
|
||||
"Infrastructure",
|
||||
"Technology",
|
||||
"Economy",
|
||||
"Social Services",
|
||||
"Other"
|
||||
]
|
||||
|
||||
export default function ProposeIssuePage() {
|
||||
const { walletConnected } = useWalletStore()
|
||||
const { toast } = useToast()
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: "",
|
||||
description: "",
|
||||
category: "",
|
||||
details: "",
|
||||
keywords: "",
|
||||
publicProposal: true
|
||||
})
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
const [proposalId, setProposalId] = useState("")
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target
|
||||
setFormData(prev => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
const handleSelectChange = (name: string) => (value: string) => {
|
||||
setFormData(prev => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
const handleSwitchChange = (checked: boolean) => {
|
||||
setFormData(prev => ({ ...prev, publicProposal: checked }))
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!walletConnected) {
|
||||
toast({
|
||||
title: "Wallet not connected",
|
||||
description: "Please connect your wallet to propose a new issue.",
|
||||
variant: "destructive"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
// This would submit to the blockchain in production
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)) // Simulate network delay
|
||||
|
||||
// Generate a random ID for the demo
|
||||
const id = Math.random().toString(36).substring(2, 10)
|
||||
setProposalId(id)
|
||||
setIsSubmitted(true)
|
||||
} catch (error) {
|
||||
console.error("Error submitting proposal:", error)
|
||||
toast({
|
||||
title: "Submission failed",
|
||||
description: "There was an error submitting your proposal. Please try again.",
|
||||
variant: "destructive"
|
||||
})
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="mb-8 flex flex-col items-center text-center">
|
||||
<CheckCircle className="h-16 w-16 text-green-500 mb-4" />
|
||||
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">Issue Proposed Successfully!</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
Your issue proposal has been submitted and is now pending review.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Proposal ID: {proposalId}
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-col sm:flex-row gap-4">
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/issues">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Issues
|
||||
</Link>
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
setIsSubmitted(false)
|
||||
setFormData({
|
||||
title: "",
|
||||
description: "",
|
||||
category: "",
|
||||
details: "",
|
||||
keywords: "",
|
||||
publicProposal: true
|
||||
})
|
||||
}}>
|
||||
Propose Another Issue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<Button variant="ghost" size="sm" asChild className="mb-4">
|
||||
<Link href="/issues">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Issues
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">Propose a New Issue</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
Suggest a topic for community discussion and potential policy recommendation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!walletConnected && (
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Wallet not connected</AlertTitle>
|
||||
<AlertDescription>
|
||||
You need to connect your wallet before proposing an issue.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Issue Information</CardTitle>
|
||||
<CardDescription>
|
||||
Provide details about the issue you want to propose for discussion.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Issue Title</Label>
|
||||
<Input
|
||||
id="title"
|
||||
name="title"
|
||||
placeholder="Enter a clear, descriptive title"
|
||||
value={formData.title}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Brief Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
placeholder="Provide a short summary of the issue (1-2 sentences)"
|
||||
value={formData.description}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This will appear in issue listings and search results.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Select
|
||||
value={formData.category}
|
||||
onValueChange={handleSelectChange("category")}
|
||||
required
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category) => (
|
||||
<SelectItem key={category} value={category}>
|
||||
{category}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="details">Detailed Description</Label>
|
||||
<Textarea
|
||||
id="details"
|
||||
name="details"
|
||||
placeholder="Provide comprehensive details about the issue, including background, significance, and potential impacts"
|
||||
className="min-h-[200px]"
|
||||
value={formData.details}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="keywords">Keywords (Optional)</Label>
|
||||
<Input
|
||||
id="keywords"
|
||||
name="keywords"
|
||||
placeholder="Enter keywords separated by commas (e.g., climate, renewable, energy)"
|
||||
value={formData.keywords}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="public-proposal"
|
||||
checked={formData.publicProposal}
|
||||
onCheckedChange={handleSwitchChange}
|
||||
/>
|
||||
<Label htmlFor="public-proposal">Make this proposal public immediately</Label>
|
||||
<Button variant="ghost" size="icon" type="button" className="h-8 w-8">
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t pt-6 flex flex-col sm:flex-row sm:justify-between gap-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
All proposals are subject to community moderation
|
||||
</p>
|
||||
<Button type="submit" disabled={isSubmitting || !walletConnected}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Submitting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
Submit Proposal
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
|
||||
<Card className="mt-8">
|
||||
<CardHeader>
|
||||
<CardTitle>Proposal Guidelines</CardTitle>
|
||||
<CardDescription>Tips for submitting an effective issue proposal</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="list-disc space-y-2 pl-4 text-muted-foreground">
|
||||
<li>Focus on issues that affect a significant portion of the community</li>
|
||||
<li>Be clear and specific about the issue you're addressing</li>
|
||||
<li>Provide objective information rather than personal opinions</li>
|
||||
<li>Consider including data or research to support your proposal</li>
|
||||
<li>Avoid duplicate issues - search first to see if your topic already exists</li>
|
||||
<li>Maintain a constructive tone that encourages productive discussion</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
31
ui/prototype/app/layout.tsx
Normal file
31
ui/prototype/app/layout.tsx
Normal file
@ -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
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>
|
||||
)
|
||||
}
|
||||
|
253
ui/prototype/app/profile/page.tsx
Normal file
253
ui/prototype/app/profile/page.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
|
3
ui/prototype/app/resolutions/[id]/loading.tsx
Normal file
3
ui/prototype/app/resolutions/[id]/loading.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return <div className="container mx-auto p-4">Loading resolution details...</div>
|
||||
}
|
529
ui/prototype/app/resolutions/[id]/page.tsx
Normal file
529
ui/prototype/app/resolutions/[id]/page.tsx
Normal file
@ -0,0 +1,529 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { use } from "react"
|
||||
import {
|
||||
ArrowLeft,
|
||||
FileText,
|
||||
Download,
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
PieChart,
|
||||
Users,
|
||||
LucideIcon,
|
||||
AlertCircle,
|
||||
BarChart3,
|
||||
Clock,
|
||||
FileBarChart,
|
||||
Share2,
|
||||
Bookmark
|
||||
} from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger
|
||||
} from "@/components/ui/tabs"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion"
|
||||
|
||||
// Mock data for resolutions with detailed content
|
||||
const mockResolutions = [
|
||||
{
|
||||
id: 12,
|
||||
title: "Education and Community Health Improvements",
|
||||
description: "A comprehensive set of recommendations for improving education access and community health services",
|
||||
summary: "This resolution addresses critical gaps in education and healthcare access by recommending the creation of free after-school programs for K-8 students and establishing community health clinics in underserved areas. These measures aim to improve educational outcomes, provide structured environments for children after school hours, and increase healthcare access for vulnerable populations.",
|
||||
date: "2024-02-15",
|
||||
contributions: [
|
||||
{
|
||||
id: 1,
|
||||
title: "After-School Programs",
|
||||
description: "Create free after-school programs for K-8 students in public schools",
|
||||
category: "Education",
|
||||
votes: { yes: 312, no: 28 },
|
||||
perspectives: 42,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Community Health Clinics",
|
||||
description: "Establish walk-in clinics in underserved areas with sliding scale fees",
|
||||
category: "Healthcare",
|
||||
votes: { yes: 278, no: 42 },
|
||||
perspectives: 36,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "Teacher Professional Development",
|
||||
description: "Increase funding for teacher training programs focused on inclusive education",
|
||||
category: "Education",
|
||||
votes: { yes: 245, no: 52 },
|
||||
perspectives: 31,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: "Preventive Health Education",
|
||||
description: "Implement community-based health education programs focused on prevention",
|
||||
category: "Healthcare",
|
||||
votes: { yes: 231, no: 38 },
|
||||
perspectives: 29,
|
||||
},
|
||||
],
|
||||
categories: ["Education", "Healthcare"],
|
||||
status: "active",
|
||||
timeline: [
|
||||
{
|
||||
date: "2023-09-01",
|
||||
event: "Perspective Collection Started",
|
||||
description: "Community members began submitting perspectives on education and healthcare needs"
|
||||
},
|
||||
{
|
||||
date: "2023-10-15",
|
||||
event: "Insights Generation",
|
||||
description: "AI-generated insights were derived from collected perspectives"
|
||||
},
|
||||
{
|
||||
date: "2023-11-15",
|
||||
event: "First Contribution Validated",
|
||||
description: "After-School Programs contribution reached consensus"
|
||||
},
|
||||
{
|
||||
date: "2023-12-03",
|
||||
event: "Second Contribution Validated",
|
||||
description: "Community Health Clinics contribution reached consensus"
|
||||
},
|
||||
{
|
||||
date: "2024-01-20",
|
||||
event: "Resolution Draft",
|
||||
description: "Initial resolution draft based on validated contributions"
|
||||
},
|
||||
{
|
||||
date: "2024-02-15",
|
||||
event: "Resolution Published",
|
||||
description: "Final resolution published after community review"
|
||||
}
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
id: "1",
|
||||
title: "Background and Context",
|
||||
content: "Recent community feedback has highlighted significant gaps in both educational support services and healthcare access, particularly in underserved neighborhoods. Data indicates that approximately 45% of K-8 students lack supervision during after-school hours, contributing to concerning trends in academic performance and youth engagement. Additionally, 38% of residents in priority neighborhoods report delaying healthcare due to lack of affordable and accessible services."
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Recommendations",
|
||||
content: "Based on community consensus, this resolution recommends: 1) Establishing free after-school programs in all public elementary and middle schools, with priority implementation in underserved areas; 2) Creating walk-in community health clinics with sliding scale fee structures in identified healthcare deserts; 3) Increasing funding for teacher professional development programs focused on inclusive education; and 4) Implementing community-based preventive health education programs.",
|
||||
subsections: [
|
||||
{
|
||||
id: "2.1",
|
||||
title: "Education Initiatives",
|
||||
content: "After-school programs should operate from 3:00-6:00 PM on school days, offering tutoring, enrichment activities, and physical recreation. Teacher professional development programs should prioritize training in inclusive education practices and trauma-informed approaches."
|
||||
},
|
||||
{
|
||||
id: "2.2",
|
||||
title: "Healthcare Initiatives",
|
||||
content: "Community health clinics should offer basic primary care, preventive services, and health education. Sliding scale fees should ensure that no resident is denied care due to inability to pay, with free services available to those meeting income requirements."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Implementation Timeline",
|
||||
content: "Phase 1 (Immediate): Begin planning and resource allocation for both education and healthcare initiatives; Phase 2 (6-12 months): Launch pilot programs in highest-need areas; Phase 3 (12-24 months): Expand to remaining target areas; Phase 4 (Ongoing): Continuous evaluation and improvement."
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "Resource Requirements",
|
||||
content: "Education Initiatives: Estimated annual budget of $4.2M for staffing, materials, and facility usage; Healthcare Initiatives: Estimated annual budget of $5.8M for facilities, medical staff, and supplies. Recommended funding sources include reallocation from existing programs, grant opportunities, and public-private partnerships."
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
title: "Expected Outcomes",
|
||||
content: "1) Improved academic performance and decreased truancy in K-8 students; 2) Reduced emergency room usage for non-emergency conditions; 3) Earlier detection and treatment of health conditions; 4) Increased community satisfaction with public services; 5) Long-term reduction in costs associated with preventable health conditions and educational remediation."
|
||||
}
|
||||
],
|
||||
impactMetrics: {
|
||||
estimatedBeneficiaries: 25000,
|
||||
costEfficiency: "High",
|
||||
implementationComplexity: "Medium",
|
||||
timeToImpact: "6-12 months",
|
||||
sustainabilityScore: 8.2,
|
||||
},
|
||||
stakeholders: ["School District", "Department of Health", "Community Organizations", "Parent Associations", "Healthcare Providers"]
|
||||
},
|
||||
// Other resolutions would be defined here
|
||||
]
|
||||
|
||||
export default function ResolutionPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
// Properly handle the params with React.use()
|
||||
const { id } = use(params)
|
||||
const resolutionId = parseInt(id)
|
||||
const [activeTab, setActiveTab] = useState("overview")
|
||||
|
||||
// Find the resolution from mock data
|
||||
const resolution = mockResolutions.find(r => r.id === resolutionId)
|
||||
|
||||
if (!resolution) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-16 text-center">
|
||||
<AlertCircle className="mx-auto h-16 w-16 text-muted-foreground mb-4" />
|
||||
<h1 className="text-2xl font-bold mb-2">Resolution Not Found</h1>
|
||||
<p className="text-muted-foreground mb-8">The resolution you're looking for doesn't exist or has been removed.</p>
|
||||
<Button asChild>
|
||||
<Link href="/resolutions">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Return to Resolutions
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Format date for display
|
||||
const formattedDate = new Date(resolution.date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<Button variant="outline" size="sm" className="mb-4" asChild>
|
||||
<Link href="/resolutions">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Resolutions
|
||||
</Link>
|
||||
</Button>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{resolution.categories.map(category => (
|
||||
<Badge key={category} className="text-xs">{category}</Badge>
|
||||
))}
|
||||
<Badge variant="outline" className="text-xs">ID: {resolution.id}</Badge>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold flex items-center gap-2">
|
||||
<FileText className="h-6 w-6 text-primary" />
|
||||
Resolution #{resolution.id}
|
||||
</h1>
|
||||
<h2 className="text-xl font-semibold mt-2">{resolution.title}</h2>
|
||||
<p className="mt-2 text-muted-foreground">{resolution.description}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-start md:items-end">
|
||||
<Badge variant={resolution.status === "active" ? "default" : "secondary"} className="mb-2">
|
||||
{resolution.status === "active" ? "Active" : "Archived"}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<Calendar className="h-4 w-4 mr-1 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Published: </span>
|
||||
<span>{formattedDate}</span>
|
||||
</div>
|
||||
<Button className="mt-2" size="sm">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download PDF
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
<Button variant="outline" size="sm">
|
||||
<Share2 className="mr-2 h-4 w-4" />
|
||||
Share
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Bookmark className="mr-2 h-4 w-4" />
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="overview" value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="content">Full Content</TabsTrigger>
|
||||
<TabsTrigger value="contributions">Contributions ({resolution.contributions.length})</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Overview Tab */}
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Executive Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm">{resolution.summary}</p>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 text-center">
|
||||
<StatCard
|
||||
icon={Users}
|
||||
label="Beneficiaries"
|
||||
value={resolution.impactMetrics.estimatedBeneficiaries.toLocaleString()}
|
||||
sublabel="estimated impact"
|
||||
/>
|
||||
<StatCard
|
||||
icon={CheckCircle}
|
||||
label="Contributions"
|
||||
value={resolution.contributions.length}
|
||||
sublabel="consensus-based"
|
||||
/>
|
||||
<StatCard
|
||||
icon={BarChart3}
|
||||
label="Cost Efficiency"
|
||||
value={resolution.impactMetrics.costEfficiency}
|
||||
sublabel="ROI rating"
|
||||
/>
|
||||
<StatCard
|
||||
icon={Clock}
|
||||
label="Time to Impact"
|
||||
value={resolution.impactMetrics.timeToImpact}
|
||||
sublabel="expected timeline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<h3 className="font-medium text-lg mb-4">Key Stakeholders</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{resolution.stakeholders.map((stakeholder, index) => (
|
||||
<Badge key={index} variant="outline" className="text-sm py-1">
|
||||
{stakeholder}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Timeline</CardTitle>
|
||||
<CardDescription>Key milestones in the resolution development</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative">
|
||||
{resolution.timeline.map((item, index) => (
|
||||
<div key={index} className="mb-6 flex gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="h-8 w-8 rounded-full bg-primary/20 flex items-center justify-center text-primary">
|
||||
{index === 0 ?
|
||||
<Users className="h-4 w-4" /> :
|
||||
index === resolution.timeline.length - 1 ?
|
||||
<CheckCircle className="h-4 w-4" /> :
|
||||
<FileBarChart className="h-4 w-4" />
|
||||
}
|
||||
</div>
|
||||
{index < resolution.timeline.length - 1 && (
|
||||
<div className="w-px h-full bg-border my-1"></div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{item.event}</p>
|
||||
<p className="text-sm text-muted-foreground">{item.description}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{new Date(item.date).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Impact Assessment</CardTitle>
|
||||
<CardDescription>Projected outcomes and metrics</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">Implementation Complexity</h4>
|
||||
<div className="flex items-center mt-2">
|
||||
<Badge variant={
|
||||
resolution.impactMetrics.implementationComplexity === "Low" ? "outline" :
|
||||
resolution.impactMetrics.implementationComplexity === "Medium" ? "secondary" :
|
||||
"destructive"
|
||||
}>
|
||||
{resolution.impactMetrics.implementationComplexity}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">Sustainability Score</h4>
|
||||
<div className="mt-2 relative pt-1">
|
||||
<div className="flex mb-2 items-center justify-between">
|
||||
<div>
|
||||
<span className="text-xs font-semibold inline-block py-1 px-2 uppercase rounded-full bg-green-200 text-green-800">
|
||||
{resolution.impactMetrics.sustainabilityScore}/10
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-hidden h-2 mb-4 text-xs flex rounded bg-muted">
|
||||
<div style={{ width: `${resolution.impactMetrics.sustainabilityScore * 10}%` }}
|
||||
className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-green-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Measures long-term viability and environmental impact
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<h4 className="text-sm font-medium mb-2">Key Outcomes</h4>
|
||||
<ul className="text-sm space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500 mt-0.5" />
|
||||
<span>Improved academic performance and decreased truancy</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500 mt-0.5" />
|
||||
<span>Reduced emergency room usage for non-emergency conditions</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-500 mt-0.5" />
|
||||
<span>Earlier detection and treatment of health conditions</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Content Tab */}
|
||||
<TabsContent value="content" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Full Resolution Text</CardTitle>
|
||||
<CardDescription>
|
||||
Resolution #{resolution.id}: {resolution.title}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
{resolution.sections.map((section) => (
|
||||
<AccordionItem key={section.id} value={section.id}>
|
||||
<AccordionTrigger className="font-medium">
|
||||
{section.id}. {section.title}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="text-sm space-y-4">
|
||||
<p>{section.content}</p>
|
||||
|
||||
{section.subsections && section.subsections.length > 0 && (
|
||||
<div className="pl-4 border-l-2 border-muted mt-4 space-y-4">
|
||||
{section.subsections.map((subsection) => (
|
||||
<div key={subsection.id}>
|
||||
<h4 className="font-medium">{subsection.id} {subsection.title}</h4>
|
||||
<p className="mt-2">{subsection.content}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Contributions Tab */}
|
||||
<TabsContent value="contributions" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contributing Insights</CardTitle>
|
||||
<CardDescription>
|
||||
Consensus-reached contributions that formed this resolution
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{resolution.contributions.map((contribution) => {
|
||||
const approvalPercentage = Math.round(
|
||||
(contribution.votes.yes / (contribution.votes.yes + contribution.votes.no)) * 100
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={contribution.id} className="border rounded-lg p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="text-xs">{contribution.category}</Badge>
|
||||
<Badge variant="outline" className="text-xs">ID: {contribution.id}</Badge>
|
||||
</div>
|
||||
<h3 className="font-medium mt-2">{contribution.title}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{contribution.description}</p>
|
||||
</div>
|
||||
<div className="text-center min-w-[80px]">
|
||||
<div className="h-16 w-16 rounded-full border-4 border-green-500 flex items-center justify-center mx-auto">
|
||||
<span className="text-lg font-bold">{approvalPercentage}%</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Consensus</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Based on </span>
|
||||
<span className="font-medium">{contribution.perspectives} perspectives</span>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" asChild>
|
||||
<Link href={`/contributions/${contribution.id}`}>
|
||||
View Details
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12 flex justify-center">
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/resolutions">Return to All Resolutions</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Stat card component
|
||||
function StatCard({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
sublabel
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
value: string | number;
|
||||
sublabel: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="p-4 border rounded-lg">
|
||||
<div className="flex justify-center mb-2">
|
||||
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold">{value}</h3>
|
||||
<p className="font-medium text-sm">{label}</p>
|
||||
<p className="text-xs text-muted-foreground">{sublabel}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
4
ui/prototype/app/resolutions/loading.tsx
Normal file
4
ui/prototype/app/resolutions/loading.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
|
310
ui/prototype/app/resolutions/page.tsx
Normal file
310
ui/prototype/app/resolutions/page.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
|
327
ui/prototype/app/submit/page.tsx
Normal file
327
ui/prototype/app/submit/page.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
|
21
ui/prototype/components.json
Normal file
21
ui/prototype/components.json
Normal file
@ -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"
|
||||
}
|
244
ui/prototype/components/app-sidebar.tsx
Normal file
244
ui/prototype/components/app-sidebar.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
|
47
ui/prototype/components/breadcrumbs.tsx
Normal file
47
ui/prototype/components/breadcrumbs.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
|
10
ui/prototype/components/connect-wallet-button.tsx
Normal file
10
ui/prototype/components/connect-wallet-button.tsx
Normal file
@ -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 />
|
||||
}
|
||||
|
91
ui/prototype/components/count-up-animation.tsx
Normal file
91
ui/prototype/components/count-up-animation.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
|
99
ui/prototype/components/sidebar-provider.tsx
Normal file
99
ui/prototype/components/sidebar-provider.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
|
11
ui/prototype/components/theme-provider.tsx
Normal file
11
ui/prototype/components/theme-provider.tsx
Normal file
@ -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>
|
||||
}
|
25
ui/prototype/components/toaster.tsx
Normal file
25
ui/prototype/components/toaster.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
|
58
ui/prototype/components/ui/accordion.tsx
Normal file
58
ui/prototype/components/ui/accordion.tsx
Normal file
@ -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 }
|
141
ui/prototype/components/ui/alert-dialog.tsx
Normal file
141
ui/prototype/components/ui/alert-dialog.tsx
Normal file
@ -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,
|
||||
}
|
59
ui/prototype/components/ui/alert.tsx
Normal file
59
ui/prototype/components/ui/alert.tsx
Normal file
@ -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 }
|
7
ui/prototype/components/ui/aspect-ratio.tsx
Normal file
7
ui/prototype/components/ui/aspect-ratio.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root
|
||||
|
||||
export { AspectRatio }
|
50
ui/prototype/components/ui/avatar.tsx
Normal file
50
ui/prototype/components/ui/avatar.tsx
Normal file
@ -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 }
|
36
ui/prototype/components/ui/badge.tsx
Normal file
36
ui/prototype/components/ui/badge.tsx
Normal file
@ -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 }
|
115
ui/prototype/components/ui/breadcrumb.tsx
Normal file
115
ui/prototype/components/ui/breadcrumb.tsx
Normal file
@ -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,
|
||||
}
|
56
ui/prototype/components/ui/button.tsx
Normal file
56
ui/prototype/components/ui/button.tsx
Normal file
@ -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 }
|
66
ui/prototype/components/ui/calendar.tsx
Normal file
66
ui/prototype/components/ui/calendar.tsx
Normal file
@ -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 }
|
79
ui/prototype/components/ui/card.tsx
Normal file
79
ui/prototype/components/ui/card.tsx
Normal file
@ -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 }
|
262
ui/prototype/components/ui/carousel.tsx
Normal file
262
ui/prototype/components/ui/carousel.tsx
Normal file
@ -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,
|
||||
}
|
365
ui/prototype/components/ui/chart.tsx
Normal file
365
ui/prototype/components/ui/chart.tsx
Normal file
@ -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,
|
||||
}
|
30
ui/prototype/components/ui/checkbox.tsx
Normal file
30
ui/prototype/components/ui/checkbox.tsx
Normal file
@ -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 }
|
11
ui/prototype/components/ui/collapsible.tsx
Normal file
11
ui/prototype/components/ui/collapsible.tsx
Normal file
@ -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 }
|
153
ui/prototype/components/ui/command.tsx
Normal file
153
ui/prototype/components/ui/command.tsx
Normal file
@ -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,
|
||||
}
|
200
ui/prototype/components/ui/context-menu.tsx
Normal file
200
ui/prototype/components/ui/context-menu.tsx
Normal file
@ -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,
|
||||
}
|
122
ui/prototype/components/ui/dialog.tsx
Normal file
122
ui/prototype/components/ui/dialog.tsx
Normal file
@ -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,
|
||||
}
|
118
ui/prototype/components/ui/drawer.tsx
Normal file
118
ui/prototype/components/ui/drawer.tsx
Normal file
@ -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,
|
||||
}
|
200
ui/prototype/components/ui/dropdown-menu.tsx
Normal file
200
ui/prototype/components/ui/dropdown-menu.tsx
Normal file
@ -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,
|
||||
}
|
178
ui/prototype/components/ui/form.tsx
Normal file
178
ui/prototype/components/ui/form.tsx
Normal file
@ -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,
|
||||
}
|
29
ui/prototype/components/ui/hover-card.tsx
Normal file
29
ui/prototype/components/ui/hover-card.tsx
Normal file
@ -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 }
|
71
ui/prototype/components/ui/input-otp.tsx
Normal file
71
ui/prototype/components/ui/input-otp.tsx
Normal file
@ -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 }
|
22
ui/prototype/components/ui/input.tsx
Normal file
22
ui/prototype/components/ui/input.tsx
Normal file
@ -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 }
|
26
ui/prototype/components/ui/label.tsx
Normal file
26
ui/prototype/components/ui/label.tsx
Normal file
@ -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 }
|
236
ui/prototype/components/ui/menubar.tsx
Normal file
236
ui/prototype/components/ui/menubar.tsx
Normal file
@ -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,
|
||||
}
|
128
ui/prototype/components/ui/navigation-menu.tsx
Normal file
128
ui/prototype/components/ui/navigation-menu.tsx
Normal file
@ -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,
|
||||
}
|
117
ui/prototype/components/ui/pagination.tsx
Normal file
117
ui/prototype/components/ui/pagination.tsx
Normal file
@ -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,
|
||||
}
|
31
ui/prototype/components/ui/popover.tsx
Normal file
31
ui/prototype/components/ui/popover.tsx
Normal file
@ -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 }
|
28
ui/prototype/components/ui/progress.tsx
Normal file
28
ui/prototype/components/ui/progress.tsx
Normal file
@ -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 }
|
44
ui/prototype/components/ui/radio-group.tsx
Normal file
44
ui/prototype/components/ui/radio-group.tsx
Normal file
@ -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 }
|
45
ui/prototype/components/ui/resizable.tsx
Normal file
45
ui/prototype/components/ui/resizable.tsx
Normal file
@ -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 }
|
48
ui/prototype/components/ui/scroll-area.tsx
Normal file
48
ui/prototype/components/ui/scroll-area.tsx
Normal file
@ -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 }
|
160
ui/prototype/components/ui/select.tsx
Normal file
160
ui/prototype/components/ui/select.tsx
Normal file
@ -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,
|
||||
}
|
31
ui/prototype/components/ui/separator.tsx
Normal file
31
ui/prototype/components/ui/separator.tsx
Normal file
@ -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 }
|
140
ui/prototype/components/ui/sheet.tsx
Normal file
140
ui/prototype/components/ui/sheet.tsx
Normal file
@ -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,
|
||||
}
|
341
ui/prototype/components/ui/sidebar.tsx
Normal file
341
ui/prototype/components/ui/sidebar.tsx
Normal file
@ -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,
|
||||
}
|
||||
|
15
ui/prototype/components/ui/skeleton.tsx
Normal file
15
ui/prototype/components/ui/skeleton.tsx
Normal file
@ -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 }
|
28
ui/prototype/components/ui/slider.tsx
Normal file
28
ui/prototype/components/ui/slider.tsx
Normal file
@ -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 }
|
31
ui/prototype/components/ui/sonner.tsx
Normal file
31
ui/prototype/components/ui/sonner.tsx
Normal file
@ -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 }
|
30
ui/prototype/components/ui/switch.tsx
Normal file
30
ui/prototype/components/ui/switch.tsx
Normal file
@ -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 }
|
||||
|
117
ui/prototype/components/ui/table.tsx
Normal file
117
ui/prototype/components/ui/table.tsx
Normal file
@ -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,
|
||||
}
|
55
ui/prototype/components/ui/tabs.tsx
Normal file
55
ui/prototype/components/ui/tabs.tsx
Normal file
@ -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 }
|
22
ui/prototype/components/ui/textarea.tsx
Normal file
22
ui/prototype/components/ui/textarea.tsx
Normal file
@ -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 }
|
112
ui/prototype/components/ui/toast.tsx
Normal file
112
ui/prototype/components/ui/toast.tsx
Normal file
@ -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,
|
||||
}
|
||||
|
35
ui/prototype/components/ui/toaster.tsx
Normal file
35
ui/prototype/components/ui/toaster.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
61
ui/prototype/components/ui/toggle-group.tsx
Normal file
61
ui/prototype/components/ui/toggle-group.tsx
Normal file
@ -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 }
|
45
ui/prototype/components/ui/toggle.tsx
Normal file
45
ui/prototype/components/ui/toggle.tsx
Normal file
@ -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 }
|
30
ui/prototype/components/ui/tooltip.tsx
Normal file
30
ui/prototype/components/ui/tooltip.tsx
Normal file
@ -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
Loading…
x
Reference in New Issue
Block a user