Initial commit - PezkuwiChain Web Governance App

This commit is contained in:
2025-10-22 18:21:46 -07:00
commit 9aab34c101
135 changed files with 24254 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+3
View File
@@ -0,0 +1,3 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
+29
View File
@@ -0,0 +1,29 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"@typescript-eslint/no-unused-vars": "off",
},
}
);
+24
View File
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PezkuwiChain Governance</title>
<meta name="description" content="A dual-token blockchain built on the Substrate framework, enabling decentralized governance and trust score-based tokenomics through sophisticated palets." />
<link rel="icon" type="image/svg+xml" href="/placeholder.svg" />
<meta property="og:title" content="PezkuwiChain Governance" />
<meta property="og:description" content="A dual-token blockchain built on the Substrate framework, enabling decentralized governance and trust score-based tokenomics through sophisticated palets." />
<meta property="og:type" content="website" />
<meta property="og:image" content="/og.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="/og.jpg" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+6655
View File
File diff suppressed because it is too large Load Diff
+90
View File
@@ -0,0 +1,90 @@
{
"name": "vite_react_shadcn_ts",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@supabase/supabase-js": "^2.49.4",
"@tanstack/react-query": "^5.56.2",
"@types/uuid": "^10.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"highlight.js": "^11.9.0",
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",
"marked": "^12.0.1",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-resizable-panels": "^2.1.3",
"react-router-dom": "^6.26.2",
"recharts": "^2.12.7",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"vaul": "^0.9.3",
"zod": "^3.23.8",
"i18next": "^23.7.6",
"react-i18next": "^14.0.0",
"i18next-browser-languagedetector": "^7.2.0"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.11",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1024.000000pt" height="1024.000000pt" viewBox="0 0 1024.000000 1024.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1790 10229 c-267 -28 -544 -118 -788 -259 -557 -319 -927 -892 -992
-1539 -14 -143 -14 -6479 0 -6622 96 -955 845 -1704 1799 -1799 143 -14 6479
-14 6622 0 693 69 1301 491 1604 1114 107 218 170 442 195 685 14 143 14 6479
0 6622 -96 955 -846 1704 -1799 1799 -129 13 -6516 11 -6641 -1z m6588 -1291
c17 -17 15 -65 -8 -198 -49 -295 -79 -421 -149 -635 -96 -293 -188 -519 -319
-783 -154 -309 -300 -548 -495 -812 -297 -401 -487 -606 -914 -988 -236 -211
-637 -475 -929 -613 -160 -75 -163 -75 -232 -1 -31 33 -93 89 -137 123 -44 34
-111 93 -150 129 -38 37 -140 127 -225 200 -148 127 -190 173 -190 212 0 23
59 176 182 473 30 72 71 173 92 225 77 194 92 229 167 387 183 384 343 613
586 841 248 231 400 353 733 585 268 187 551 338 950 507 128 54 344 137 495
190 50 17 131 47 180 65 271 101 338 118 363 93z m-3327 -1925 c19 -19 2 -75
-45 -149 -91 -146 -212 -370 -348 -644 -174 -351 -236 -488 -300 -655 -27 -71
-56 -146 -63 -165 -8 -19 -14 -57 -15 -85 0 -62 -17 -124 -35 -131 -7 -3 -36
6 -64 20 -54 28 -100 33 -134 14 -20 -10 -283 -249 -562 -510 -66 -62 -172
-152 -235 -199 -124 -93 -429 -288 -600 -383 -58 -33 -150 -87 -205 -121 -147
-92 -273 -158 -290 -151 -22 8 -19 64 6 111 11 22 44 76 73 120 30 44 84 130
121 190 38 61 101 160 140 220 40 61 85 133 100 160 15 28 35 61 45 75 17 24
180 316 259 464 22 39 69 127 106 196 76 141 136 255 322 615 283 546 456 802
568 845 17 6 112 25 213 41 100 16 238 40 305 53 67 14 145 27 172 31 44 6
233 36 305 49 44 8 150 0 161 -11z m1809 -1548 c9 -11 11 -49 6 -147 -4 -73
-11 -227 -16 -343 -22 -463 -48 -828 -70 -962 -19 -115 -49 -192 -92 -239 -83
-90 -251 -231 -726 -608 -415 -329 -485 -385 -723 -586 -446 -373 -1019 -912
-1278 -1200 -124 -138 -141 -153 -163 -145 -36 14 -40 52 -33 290 21 759 70
941 378 1383 249 359 612 755 1052 1148 110 98 202 181 205 184 3 3 32 27 65
55 33 27 107 92 165 145 58 52 184 165 280 250 95 85 237 214 314 285 78 72
179 162 226 199 47 38 108 90 135 116 79 73 235 190 255 190 4 0 13 -7 20 -15z
m-2219 -661 c39 -19 95 -86 130 -156 43 -85 52 -193 20 -255 -72 -141 -514
-582 -1031 -1028 -96 -82 -197 -170 -225 -195 -28 -25 -104 -90 -170 -145
-127 -105 -310 -262 -385 -331 -25 -23 -63 -57 -85 -75 -22 -19 -51 -44 -65
-57 -14 -13 -42 -32 -63 -43 -34 -18 -38 -18 -44 -3 -7 19 44 119 130 254 97
151 298 456 376 568 214 309 263 378 377 535 276 381 483 638 612 758 61 57
108 90 184 130 140 71 171 77 239 43z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+14
View File
@@ -0,0 +1,14 @@
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Twitterbot
Allow: /
User-agent: facebookexternalhit
Allow: /
User-agent: *
Allow: /
+53
View File
@@ -0,0 +1,53 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em rgba(20, 184, 166, 0.6));
}
.logo.react:hover {
filter: drop-shadow(0 0 2em rgba(20, 184, 166, 0.6));
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
border-radius: 0.5rem;
border: 1px solid rgba(20, 184, 166, 0.1);
background-color: rgba(20, 184, 166, 0.02);
transition: all 0.3s ease;
}
.card:hover {
border-color: rgba(20, 184, 166, 0.3);
box-shadow: 0 4px 12px rgba(20, 184, 166, 0.1);
}
.read-the-docs {
color: #5f7676;
}
+65
View File
@@ -0,0 +1,65 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { ThemeProvider } from '@/components/theme-provider';
import Index from '@/pages/Index';
import Login from '@/pages/Login';
import Dashboard from '@/pages/Dashboard';
import EmailVerification from '@/pages/EmailVerification';
import PasswordReset from '@/pages/PasswordReset';
import ProfileSettings from '@/pages/ProfileSettings';
import AdminPanel from '@/pages/AdminPanel';
import { AppProvider } from '@/contexts/AppContext';
import { WalletProvider } from '@/contexts/WalletContext';
import { WebSocketProvider } from '@/contexts/WebSocketContext';
import { IdentityProvider } from '@/contexts/IdentityContext';
import { AuthProvider } from '@/contexts/AuthContext';
import { ProtectedRoute } from '@/components/ProtectedRoute';
import NotFound from '@/pages/NotFound';
import { Toaster } from '@/components/ui/toaster';
import './App.css';
import './i18n/config';
function App() {
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<AuthProvider>
<AppProvider>
<WalletProvider>
<WebSocketProvider>
<IdentityProvider>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/email-verification" element={<EmailVerification />} />
<Route path="/reset-password" element={<PasswordReset />} />
<Route path="/" element={<Index />} />
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
<Route path="/profile-settings" element={
<ProtectedRoute>
<ProfileSettings />
</ProtectedRoute>
} />
<Route path="/admin" element={
<ProtectedRoute requireAdmin>
<AdminPanel />
</ProtectedRoute>
} />
<Route path="*" element={<NotFound />} />
</Routes>
</Router>
</IdentityProvider>
</WebSocketProvider>
</WalletProvider>
</AppProvider>
</AuthProvider>
<Toaster />
</ThemeProvider>
);
}
export default App;
+504
View File
@@ -0,0 +1,504 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '@/contexts/AuthContext';
import HeroSection from './HeroSection';
import TokenomicsSection from './TokenomicsSection';
import TokenSwap from './TokenSwap';
import PalletsGrid from './PalletsGrid';
import TeamSection from './TeamSection';
import ChainSpecs from './ChainSpecs';
import TrustScoreCalculator from './TrustScoreCalculator';
import { WalletButton } from './wallet/WalletButton';
import { WalletModal } from './wallet/WalletModal';
import { LanguageSwitcher } from './LanguageSwitcher';
import NotificationBell from './notifications/NotificationBell';
import ProposalWizard from './proposals/ProposalWizard';
import DelegationManager from './delegation/DelegationManager';
import { ForumOverview } from './forum/ForumOverview';
import { ModerationPanel } from './forum/ModerationPanel';
import { TreasuryOverview } from './treasury/TreasuryOverview';
import { FundingProposal } from './treasury/FundingProposal';
import { SpendingHistory } from './treasury/SpendingHistory';
import { MultiSigApproval } from './treasury/MultiSigApproval';
import { Github, FileText, ExternalLink, Shield, Award, User, FileEdit, Users2, MessageSquare, ShieldCheck, Wifi, WifiOff, Wallet, DollarSign, PiggyBank, History, Key, TrendingUp, ArrowRightLeft, Lock, LogIn, LayoutDashboard, Settings, UserCog, Repeat } from 'lucide-react';
import GovernanceInterface from './GovernanceInterface';
import RewardDistribution from './RewardDistribution';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useWebSocket } from '@/contexts/WebSocketContext';
import { StakingDashboard } from './staking/StakingDashboard';
import { P2PMarket } from './p2p/P2PMarket';
import { MultiSigWallet } from './wallet/MultiSigWallet';
import { useWallet } from '@/contexts/WalletContext';
import { supabase } from '@/lib/supabase';
const AppLayout: React.FC = () => {
const navigate = useNavigate();
const [walletModalOpen, setWalletModalOpen] = useState(false);
const [transactionModalOpen, setTransactionModalOpen] = useState(false);
const { user, signOut } = useAuth();
const [showProposalWizard, setShowProposalWizard] = useState(false);
const [showDelegation, setShowDelegation] = useState(false);
const [showForum, setShowForum] = useState(false);
const [showModeration, setShowModeration] = useState(false);
const [showTreasury, setShowTreasury] = useState(false);
const [treasuryTab, setTreasuryTab] = useState('overview');
const [showStaking, setShowStaking] = useState(false);
const [showP2P, setShowP2P] = useState(false);
const [showMultiSig, setShowMultiSig] = useState(false);
const [showTokenSwap, setShowTokenSwap] = useState(false);
const { t } = useTranslation();
const { isConnected } = useWebSocket();
const { account } = useWallet();
const [isAdmin, setIsAdmin] = useState(false);
// Check if user is admin
React.useEffect(() => {
const checkAdminStatus = async () => {
if (user) {
const { data } = await supabase
.from('admin_roles')
.select('role')
.eq('user_id', user.id)
.single();
setIsAdmin(!!data);
}
};
checkAdminStatus();
}, [user]);
return (
<div className="min-h-screen bg-gray-950 text-white">
{/* Navigation */}
<nav className="fixed top-0 w-full z-40 bg-gray-950/90 backdrop-blur-md border-b border-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center">
<span className="text-xl font-bold bg-gradient-to-r from-green-500 to-yellow-400 bg-clip-text text-transparent">
PezkuwiChain
</span>
</div>
<div className="hidden md:flex items-center space-x-8">
{user ? (
<>
<button
onClick={() => navigate('/dashboard')}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<LayoutDashboard className="w-4 h-4" />
{t('nav.dashboard', 'Dashboard')}
</button>
<button
onClick={() => navigate('/profile-settings')}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<Settings className="w-4 h-4" />
{t('nav.settings', 'Settings')}
</button>
{isAdmin && (
<button
onClick={() => navigate('/admin')}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<UserCog className="w-4 h-4" />
{t('nav.admin', 'Admin')}
</button>
)}
<button
onClick={() => setShowProposalWizard(true)}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<FileEdit className="w-4 h-4" />
{t('nav.proposals')}
</button>
<button
onClick={() => setShowDelegation(true)}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<Users2 className="w-4 h-4" />
{t('nav.delegation')}
</button>
<button
onClick={() => setShowForum(true)}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<MessageSquare className="w-4 h-4" />
{t('nav.forum')}
</button>
<button
onClick={() => {
setShowTreasury(true);
setTreasuryTab('overview');
}}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<PiggyBank className="w-4 h-4" />
{t('nav.treasury')}
</button>
<button
onClick={() => setShowModeration(true)}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<ShieldCheck className="w-4 h-4" />
{t('nav.moderation')}
</button>
<button
onClick={async () => {
await signOut();
navigate('/login');
}}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<LogIn className="w-4 h-4 rotate-180" />
{t('nav.logout', 'Logout')}
</button>
</>
) : (
<button
onClick={() => navigate('/login')}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2"
>
<LogIn className="w-4 h-4" />
{t('nav.login', 'Login')}
</button>
)}
<button
onClick={() => document.getElementById('governance')?.scrollIntoView({ behavior: 'smooth' })}
className="text-gray-300 hover:text-white transition-colors"
>
{t('nav.governance')}
</button>
<button
onClick={() => setShowStaking(true)}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<TrendingUp className="w-4 h-4" />
Staking
</button>
<button
onClick={() => setShowP2P(true)}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<ArrowRightLeft className="w-4 h-4" />
P2P
</button>
<button
onClick={() => setShowTokenSwap(true)}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<Repeat className="w-4 h-4" />
Token Swap
</button>
<button
onClick={() => setShowMultiSig(true)}
className="text-gray-300 hover:text-white transition-colors flex items-center gap-1"
>
<Lock className="w-4 h-4" />
MultiSig
</button>
<a
href="https://docs.pezkuwichain.io"
target="_blank"
rel="noopener noreferrer"
className="text-gray-300 hover:text-white transition-colors"
>
{t('nav.docs')}
</a>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
{isConnected ? (
<div className="flex items-center text-green-500 text-sm">
<Wifi className="w-4 h-4 mr-1" />
<span className="hidden sm:inline">Live</span>
</div>
) : (
<div className="flex items-center text-gray-500 text-sm">
<WifiOff className="w-4 h-4 mr-1" />
<span className="hidden sm:inline">Offline</span>
</div>
)}
</div>
<NotificationBell />
<LanguageSwitcher />
<WalletButton />
<a
href="https://github.com/pezkuwichain"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-white transition-colors"
>
<Github className="w-5 h-5" />
</a>
</div>
</div>
</div>
</nav>
{/* Main Content */}
<main>
{/* Conditional Rendering for Features */}
{showProposalWizard ? (
<ProposalWizard
onComplete={(proposal) => {
console.log('Proposal created:', proposal);
setShowProposalWizard(false);
}}
onCancel={() => setShowProposalWizard(false)}
/>
) : showDelegation ? (
<DelegationManager />
) : showForum ? (
<div className="pt-20 min-h-screen bg-gray-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ForumOverview />
</div>
</div>
) : showModeration ? (
<div className="pt-20 min-h-screen bg-gray-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ModerationPanel />
</div>
</div>
) : showTreasury ? (
<div className="pt-20 min-h-screen bg-gray-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
{t('treasury.title', 'Treasury Management')}
</h2>
<p className="text-gray-400 text-lg max-w-3xl mx-auto">
{t('treasury.subtitle', 'Track funds, submit proposals, and manage community resources')}
</p>
</div>
<Tabs value={treasuryTab} onValueChange={setTreasuryTab} className="w-full">
<TabsList className="grid w-full grid-cols-4 mb-8">
<TabsTrigger value="overview" className="flex items-center gap-2">
<PiggyBank className="w-4 h-4" />
{t('treasury.overview', 'Overview')}
</TabsTrigger>
<TabsTrigger value="proposals" className="flex items-center gap-2">
<DollarSign className="w-4 h-4" />
{t('treasury.proposals', 'Funding Proposals')}
</TabsTrigger>
<TabsTrigger value="history" className="flex items-center gap-2">
<History className="w-4 h-4" />
{t('treasury.history', 'Spending History')}
</TabsTrigger>
<TabsTrigger value="approvals" className="flex items-center gap-2">
<Key className="w-4 h-4" />
{t('treasury.approvals', 'Multi-Sig Approvals')}
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-6">
<TreasuryOverview />
</TabsContent>
<TabsContent value="proposals" className="mt-6">
<FundingProposal />
</TabsContent>
<TabsContent value="history" className="mt-6">
<SpendingHistory />
</TabsContent>
<TabsContent value="approvals" className="mt-6">
<MultiSigApproval />
</TabsContent>
</Tabs>
</div>
</div>
) : showStaking ? (
<div className="pt-20 min-h-screen bg-gray-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
Staking Rewards
</h2>
<p className="text-gray-400 text-lg max-w-3xl mx-auto">
Stake your tokens and earn rewards
</p>
</div>
<StakingDashboard />
</div>
</div>
) : showP2P ? (
<div className="pt-20 min-h-screen bg-gray-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
P2P Trading Market
</h2>
<p className="text-gray-400 text-lg max-w-3xl mx-auto">
Trade tokens directly with other users
</p>
</div>
<P2PMarket />
</div>
</div>
) : showTokenSwap ? (
<div className="pt-20 min-h-screen bg-gray-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-500 via-pink-400 to-yellow-500 bg-clip-text text-transparent">
PEZ/HEZ Token Swap
</h2>
<p className="text-gray-400 text-lg max-w-3xl mx-auto">
Swap between PEZ and HEZ tokens instantly with real-time rates
</p>
</div>
<TokenSwap />
</div>
</div>
) : showMultiSig ? (
<div className="pt-20 min-h-screen bg-gray-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
Multi-Signature Wallet
</h2>
<p className="text-gray-400 text-lg max-w-3xl mx-auto">
Secure your funds with multi-signature protection
</p>
</div>
<MultiSigWallet />
</div>
</div>
) : (
<>
<HeroSection />
<PalletsGrid />
<TokenomicsSection />
<div id="trust-calculator">
<TrustScoreCalculator />
</div>
<div id="chain-specs">
<ChainSpecs />
</div>
<div id="governance">
<GovernanceInterface />
</div>
<div id="rewards">
<RewardDistribution />
</div>
</>
)}
{(showProposalWizard || showDelegation || showForum || showModeration || showTreasury || showStaking || showP2P || showMultiSig || showTokenSwap) && (
<div className="fixed bottom-8 right-8 z-50">
<button
onClick={() => {
setShowProposalWizard(false);
setShowDelegation(false);
setShowForum(false);
setShowModeration(false);
setShowTreasury(false);
setShowStaking(false);
setShowP2P(false);
setShowMultiSig(false);
setShowTokenSwap(false);
}}
className="bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-full shadow-lg flex items-center gap-2 transition-all"
>
{t('common.backToHome')}
</button>
</div>
)}
</main>
{/* Wallet Modal */}
<WalletModal isOpen={walletModalOpen} onClose={() => setWalletModalOpen(false)} />
{/* Footer */}
<footer className="bg-gray-950 border-t border-gray-800 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<h3 className="text-lg font-semibold mb-4 bg-gradient-to-r from-green-500 to-yellow-400 bg-clip-text text-transparent">
PezkuwiChain
</h3>
<p className="text-gray-400 text-sm">
{t('footer.description', 'Decentralized governance for Kurdistan')}
</p>
</div>
<div>
<h4 className="text-white font-semibold mb-4">{t('footer.about')}</h4>
<ul className="space-y-2">
<li>
<a href="#" className="text-gray-400 hover:text-white text-sm flex items-center">
{t('nav.docs')}
<ExternalLink className="w-3 h-3 ml-1" />
</a>
</li>
<li>
<a href="#" className="text-gray-400 hover:text-white text-sm">
GitHub
</a>
</li>
</ul>
</div>
<div>
<h4 className="text-white font-semibold mb-4">{t('footer.developers')}</h4>
<ul className="space-y-2">
<li>
<a href="#" className="text-gray-400 hover:text-white text-sm">
API
</a>
</li>
<li>
<a href="#" className="text-gray-400 hover:text-white text-sm">
SDK
</a>
</li>
</ul>
</div>
<div>
<h4 className="text-white font-semibold mb-4">{t('footer.community')}</h4>
<ul className="space-y-2">
<li>
<a href="#" className="text-gray-400 hover:text-white text-sm">
Discord
</a>
</li>
<li>
<a href="#" className="text-gray-400 hover:text-white text-sm">
Twitter
</a>
</li>
<li>
<a href="#" className="text-gray-400 hover:text-white text-sm">
Telegram
</a>
</li>
</ul>
</div>
</div>
<div className="mt-8 pt-8 border-t border-gray-800 text-center">
<p className="text-gray-400 text-sm">
© 2024 PezkuwiChain. {t('footer.rights')}
</p>
</div>
</div>
</footer>
</div>
);
};
export default AppLayout;
+257
View File
@@ -0,0 +1,257 @@
import React, { useState } from 'react';
import { Server, Globe, TestTube, Code, Wifi, Copy, Check } from 'lucide-react';
interface ChainSpec {
id: string;
name: string;
type: 'Live' | 'Development' | 'Local';
icon: React.ReactNode;
endpoint: string;
chainId: string;
validators: number;
features: string[];
color: string;
}
const chainSpecs: ChainSpec[] = [
{
id: 'mainnet',
name: 'PezkuwiChain Mainnet',
type: 'Live',
icon: <Globe className="w-5 h-5" />,
endpoint: 'wss://mainnet.pezkuwichain.io',
chainId: '0x1234...abcd',
validators: 100,
features: ['Production', 'Real Tokenomics', 'Full Security'],
color: 'from-purple-500 to-purple-600'
},
{
id: 'staging',
name: 'PezkuwiChain Staging',
type: 'Live',
icon: <Server className="w-5 h-5" />,
endpoint: 'wss://staging.pezkuwichain.io',
chainId: '0x5678...efgh',
validators: 20,
features: ['Pre-production', 'Testing Features', 'Beta Access'],
color: 'from-cyan-500 to-cyan-600'
},
{
id: 'testnet',
name: 'Real Testnet',
type: 'Live',
icon: <TestTube className="w-5 h-5" />,
endpoint: 'wss://testnet.pezkuwichain.io',
chainId: '0x9abc...ijkl',
validators: 8,
features: ['Test Tokens', 'Full Features', 'Public Testing'],
color: 'from-teal-500 to-teal-600'
},
{
id: 'beta',
name: 'Beta Testnet',
type: 'Live',
icon: <TestTube className="w-5 h-5" />,
endpoint: 'wss://beta.pezkuwichain.io',
chainId: '0xdef0...mnop',
validators: 4,
features: ['Experimental', 'New Features', 'Limited Access'],
color: 'from-orange-500 to-orange-600'
},
{
id: 'development',
name: 'Development',
type: 'Development',
icon: <Code className="w-5 h-5" />,
endpoint: 'ws://127.0.0.1:9944',
chainId: '0xlocal...dev',
validators: 1,
features: ['Single Node', 'Fast Block Time', 'Dev Tools'],
color: 'from-green-500 to-green-600'
},
{
id: 'local',
name: 'Local Testnet',
type: 'Local',
icon: <Wifi className="w-5 h-5" />,
endpoint: 'ws://127.0.0.1:9945',
chainId: '0xlocal...test',
validators: 2,
features: ['Multi-node', 'Local Testing', 'Custom Config'],
color: 'from-indigo-500 to-indigo-600'
}
];
const ChainSpecs: React.FC = () => {
const [copiedId, setCopiedId] = useState<string | null>(null);
const [selectedSpec, setSelectedSpec] = useState<ChainSpec>(chainSpecs[0]);
const copyToClipboard = (text: string, id: string) => {
navigator.clipboard.writeText(text);
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
};
return (
<section className="py-20 bg-gray-900/50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
Chain Specifications
</h2>
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
Multiple network environments for development, testing, and production
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
{chainSpecs.map((spec) => (
<div
key={spec.id}
onClick={() => setSelectedSpec(spec)}
className={`cursor-pointer p-4 rounded-xl border transition-all ${
selectedSpec.id === spec.id
? 'bg-gray-900 border-purple-500'
: 'bg-gray-950/50 border-gray-800 hover:border-gray-700'
}`}
>
<div className="flex items-start justify-between mb-3">
<div className={`p-2 rounded-lg bg-gradient-to-br ${spec.color} bg-opacity-20`}>
{spec.icon}
</div>
<span className={`px-2 py-1 text-xs rounded-full ${
spec.type === 'Live' ? 'bg-green-900/30 text-green-400' :
spec.type === 'Development' ? 'bg-yellow-900/30 text-yellow-400' :
'bg-blue-900/30 text-blue-400'
}`}>
{spec.type}
</span>
</div>
<h3 className="text-white font-semibold mb-2">{spec.name}</h3>
<div className="flex items-center text-sm text-gray-400">
<Server className="w-3 h-3 mr-1" />
<span>{spec.validators} validators</span>
</div>
</div>
))}
</div>
{/* Selected Chain Details */}
<div className="bg-gray-950/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<h3 className="text-xl font-semibold text-white mb-4 flex items-center">
<div className={`p-2 rounded-lg bg-gradient-to-br ${selectedSpec.color} bg-opacity-20 mr-3`}>
{selectedSpec.icon}
</div>
{selectedSpec.name}
</h3>
<div className="space-y-4">
<div>
<label className="text-gray-400 text-sm">WebSocket Endpoint</label>
<div className="flex items-center mt-1">
<code className="flex-1 p-3 bg-gray-900 rounded-lg text-cyan-400 font-mono text-sm">
{selectedSpec.endpoint}
</code>
<button
onClick={() => copyToClipboard(selectedSpec.endpoint, `endpoint-${selectedSpec.id}`)}
className="ml-2 p-2 text-gray-400 hover:text-white transition-colors"
>
{copiedId === `endpoint-${selectedSpec.id}` ?
<Check className="w-5 h-5 text-green-400" /> :
<Copy className="w-5 h-5" />
}
</button>
</div>
</div>
<div>
<label className="text-gray-400 text-sm">Chain ID</label>
<div className="flex items-center mt-1">
<code className="flex-1 p-3 bg-gray-900 rounded-lg text-purple-400 font-mono text-sm">
{selectedSpec.chainId}
</code>
<button
onClick={() => copyToClipboard(selectedSpec.chainId, `chainid-${selectedSpec.id}`)}
className="ml-2 p-2 text-gray-400 hover:text-white transition-colors"
>
{copiedId === `chainid-${selectedSpec.id}` ?
<Check className="w-5 h-5 text-green-400" /> :
<Copy className="w-5 h-5" />
}
</button>
</div>
</div>
<div>
<label className="text-gray-400 text-sm mb-2 block">Features</label>
<div className="flex flex-wrap gap-2">
{selectedSpec.features.map((feature) => (
<span
key={feature}
className="px-3 py-1 bg-gray-900 text-gray-300 text-sm rounded-full"
>
{feature}
</span>
))}
</div>
</div>
</div>
</div>
<div>
<h4 className="text-lg font-semibold text-white mb-4">Connection Example</h4>
<div className="bg-gray-900 rounded-lg p-4 font-mono text-sm">
<div className="text-gray-400 mb-2">// Using @polkadot/api</div>
<div className="text-cyan-400">import</div>
<div className="text-white ml-2">{'{ ApiPromise, WsProvider }'}</div>
<div className="text-cyan-400">from</div>
<div className="text-green-400 mb-3">'@polkadot/api';</div>
<div className="text-cyan-400">const</div>
<div className="text-white ml-2">provider =</div>
<div className="text-cyan-400 ml-2">new</div>
<div className="text-yellow-400 ml-2">WsProvider(</div>
<div className="text-green-400 ml-4">'{selectedSpec.endpoint}'</div>
<div className="text-yellow-400">);</div>
<div className="text-cyan-400 mt-2">const</div>
<div className="text-white ml-2">api =</div>
<div className="text-cyan-400 ml-2">await</div>
<div className="text-yellow-400 ml-2">ApiPromise.create</div>
<div className="text-white">({'{ provider }'});</div>
</div>
<div className="mt-4 p-4 bg-kurdish-green/20 rounded-lg border border-kurdish-green/30">
<h5 className="text-kurdish-green font-semibold mb-2">Network Stats</h5>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-400">Block Time:</span>
<span className="text-white ml-2">6s</span>
</div>
<div>
<span className="text-gray-400">Finality:</span>
<span className="text-white ml-2">GRANDPA</span>
</div>
<div>
<span className="text-gray-400">Consensus:</span>
<span className="text-white ml-2">Aura</span>
</div>
<div>
<span className="text-gray-400">Runtime:</span>
<span className="text-white ml-2">v1.0.0</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default ChainSpecs;
+87
View File
@@ -0,0 +1,87 @@
import React, { useState } from 'react';
import { TrendingUp, FileText, Users, Shield, Vote, History } from 'lucide-react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import GovernanceOverview from './governance/GovernanceOverview';
import ProposalsList from './governance/ProposalsList';
import ElectionsInterface from './governance/ElectionsInterface';
const GovernanceInterface: React.FC = () => {
const [activeTab, setActiveTab] = useState('overview');
return (
<section className="py-20 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4">
<span className="bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
On-Chain Governance
</span>
</h2>
<p className="text-gray-400 text-lg max-w-3xl mx-auto">
Participate in PezkuwiChain's decentralized governance. Vote on proposals, elect representatives, and shape the future of the network.
</p>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid grid-cols-3 lg:grid-cols-6 gap-2 bg-gray-900/50 p-1 rounded-lg">
<TabsTrigger value="overview" className="flex items-center space-x-2">
<TrendingUp className="w-4 h-4" />
<span>Overview</span>
</TabsTrigger>
<TabsTrigger value="proposals" className="flex items-center space-x-2">
<FileText className="w-4 h-4" />
<span>Proposals</span>
</TabsTrigger>
<TabsTrigger value="elections" className="flex items-center space-x-2">
<Users className="w-4 h-4" />
<span>Elections</span>
</TabsTrigger>
<TabsTrigger value="delegation" className="flex items-center space-x-2">
<Shield className="w-4 h-4" />
<span>Delegation</span>
</TabsTrigger>
<TabsTrigger value="voting" className="flex items-center space-x-2">
<Vote className="w-4 h-4" />
<span>My Votes</span>
</TabsTrigger>
<TabsTrigger value="history" className="flex items-center space-x-2">
<History className="w-4 h-4" />
<span>History</span>
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-6">
<GovernanceOverview />
</TabsContent>
<TabsContent value="proposals" className="mt-6">
<ProposalsList />
</TabsContent>
<TabsContent value="elections" className="mt-6">
<ElectionsInterface />
</TabsContent>
<TabsContent value="delegation" className="mt-6">
<div className="text-center py-12 text-gray-400">
Delegation interface coming soon...
</div>
</TabsContent>
<TabsContent value="voting" className="mt-6">
<div className="text-center py-12 text-gray-400">
Voting history coming soon...
</div>
</TabsContent>
<TabsContent value="history" className="mt-6">
<div className="text-center py-12 text-gray-400">
Governance history coming soon...
</div>
</TabsContent>
</Tabs>
</div>
</section>
);
};
export default GovernanceInterface;
+77
View File
@@ -0,0 +1,77 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ChevronRight, Cpu, GitBranch, Shield } from 'lucide-react';
const HeroSection: React.FC = () => {
const { t } = useTranslation();
return (
<section className="relative min-h-screen flex items-center justify-center overflow-hidden bg-gray-950">
{/* Kurdish Flag Background */}
<div className="absolute inset-0">
<img
src="https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760373625599_6626c9cb.webp"
alt="Kurdish Flag"
className="w-full h-full object-cover opacity-30"
/>
<div className="absolute inset-0 bg-gradient-to-b from-green-600/20 via-gray-950/70 to-gray-950"></div>
</div>
{/* Content */}
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div className="mb-8 inline-flex items-center px-4 py-2 rounded-full bg-green-600/20 backdrop-blur-sm border border-green-500/30">
<Shield className="w-4 h-4 text-yellow-400 mr-2" />
<span className="text-yellow-400 text-sm font-medium">Substrate Parachain v1.0</span>
</div>
<h1 className="text-5xl md:text-7xl font-bold mb-6 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
PezkuwiChain
</h1>
<p className="text-xl md:text-2xl text-gray-300 mb-8 max-w-3xl mx-auto">
{t('hero.title')}
</p>
<p className="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
{t('hero.subtitle')}
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12 max-w-4xl mx-auto">
<div className="bg-gray-900/50 backdrop-blur-sm rounded-lg border border-green-500/30 p-4">
<div className="text-2xl font-bold text-green-400">127</div>
<div className="text-sm text-gray-400">{t('hero.stats.activeProposals')}</div>
</div>
<div className="bg-gray-900/50 backdrop-blur-sm rounded-lg border border-yellow-400/30 p-4">
<div className="text-2xl font-bold text-yellow-400">3,482</div>
<div className="text-sm text-gray-400">{t('hero.stats.totalVoters')}</div>
</div>
<div className="bg-gray-900/50 backdrop-blur-sm rounded-lg border border-red-500/30 p-4">
<div className="text-2xl font-bold text-red-400">2.1M</div>
<div className="text-sm text-gray-400">{t('hero.stats.tokensStaked')}</div>
</div>
<div className="bg-gray-900/50 backdrop-blur-sm rounded-lg border border-green-500/30 p-4">
<div className="text-2xl font-bold text-green-400">95%</div>
<div className="text-sm text-gray-400">{t('hero.stats.trustScore')}</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<button
onClick={() => document.getElementById('governance')?.scrollIntoView({ behavior: 'smooth' })}
className="px-8 py-4 bg-gradient-to-r from-green-600 to-yellow-400 text-white font-semibold rounded-lg hover:from-green-700 hover:to-yellow-500 transition-all transform hover:scale-105 flex items-center justify-center group"
>
{t('hero.exploreGovernance')}
<ChevronRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
</button>
<button
onClick={() => document.getElementById('identity')?.scrollIntoView({ behavior: 'smooth' })}
className="px-8 py-4 bg-gray-900/50 backdrop-blur-sm text-white font-semibold rounded-lg border border-red-500/50 hover:bg-red-500/10 transition-all"
>
{t('hero.learnMore')}
</button>
</div>
</div>
</section>
);
};
export default HeroSection;
+59
View File
@@ -0,0 +1,59 @@
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Globe } from 'lucide-react';
import { languages } from '@/i18n/config';
import { useEffect } from 'react';
export function LanguageSwitcher() {
const { i18n } = useTranslation();
useEffect(() => {
// Update document direction based on language
const currentLang = languages[i18n.language as keyof typeof languages];
if (currentLang) {
document.documentElement.dir = currentLang.dir;
document.documentElement.lang = i18n.language;
}
}, [i18n.language]);
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
const lang = languages[lng as keyof typeof languages];
if (lang) {
document.documentElement.dir = lang.dir;
document.documentElement.lang = lng;
}
};
const currentLanguage = languages[i18n.language as keyof typeof languages] || languages.en;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="gap-2">
<Globe className="h-4 w-4" />
<span className="hidden sm:inline">{currentLanguage.name}</span>
<span className="text-lg">{currentLanguage.flag}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{Object.entries(languages).map(([code, lang]) => (
<DropdownMenuItem
key={code}
onClick={() => changeLanguage(code)}
className={`cursor-pointer ${i18n.language === code ? 'bg-yellow-100 dark:bg-yellow-900' : ''}`}
>
<span className="text-lg mr-2">{lang.flag}</span>
<span>{lang.name}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
+164
View File
@@ -0,0 +1,164 @@
import React, { useState } from 'react';
import { Code, Database, TrendingUp, Gift, UserCheck, Award } from 'lucide-react';
interface Pallet {
id: string;
name: string;
icon: React.ReactNode;
description: string;
image: string;
extrinsics: string[];
storage: string[];
}
const pallets: Pallet[] = [
{
id: 'pez-treasury',
name: 'PEZ Treasury',
icon: <Database className="w-6 h-6" />,
description: 'Manages token distribution with 48-month synthetic halving mechanism',
image: 'https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760315321470_3d093f4f.webp',
extrinsics: ['initialize_treasury', 'release_monthly_funds', 'force_genesis_distribution'],
storage: ['HalvingInfo', 'MonthlyReleases', 'TreasuryStartBlock']
},
{
id: 'trust',
name: 'Trust Score',
icon: <TrendingUp className="w-6 h-6" />,
description: 'Calculates weighted trust scores from multiple components',
image: 'https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760315323202_06631fb8.webp',
extrinsics: ['force_recalculate_trust_score', 'update_all_trust_scores', 'periodic_trust_score_update'],
storage: ['TrustScores', 'TotalActiveTrustScore', 'BatchUpdateInProgress']
},
{
id: 'staking-score',
name: 'Staking Score',
icon: <Award className="w-6 h-6" />,
description: 'Time-based staking multipliers from 1.0x to 2.0x over 12 months',
image: 'https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760315324943_84216eda.webp',
extrinsics: ['start_score_tracking'],
storage: ['StakingStartBlock']
},
{
id: 'pez-rewards',
name: 'PEZ Rewards',
icon: <Gift className="w-6 h-6" />,
description: 'Monthly epoch-based reward distribution system',
image: 'https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760315326731_ca5f9a92.webp',
extrinsics: ['initialize_rewards_system', 'record_trust_score', 'finalize_epoch', 'claim_reward'],
storage: ['EpochInfo', 'EpochRewardPools', 'UserEpochScores', 'ClaimedRewards']
}
];
const PalletsGrid: React.FC = () => {
const [selectedPallet, setSelectedPallet] = useState<Pallet | null>(null);
return (
<section id="pallets" className="py-20 bg-gray-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
Core Runtime Pallets
</h2>
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
Modular blockchain components powering PezkuwiChain's advanced features
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{pallets.map((pallet) => (
<div
key={pallet.id}
onClick={() => setSelectedPallet(pallet)}
className="group relative bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 hover:border-purple-500/50 transition-all cursor-pointer overflow-hidden"
>
{/* Background Glow */}
<div className="absolute inset-0 bg-gradient-to-br from-purple-900/20 to-cyan-900/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
<div className="relative p-6">
<div className="flex items-start space-x-4">
<img
src={pallet.image}
alt={pallet.name}
className="w-20 h-20 rounded-lg object-cover"
/>
<div className="flex-1">
<div className="flex items-center mb-2">
<div className="p-2 bg-gradient-to-br from-purple-600/20 to-cyan-600/20 rounded-lg mr-3">
{pallet.icon}
</div>
<h3 className="text-xl font-semibold text-white">{pallet.name}</h3>
</div>
<p className="text-gray-400 text-sm mb-4">{pallet.description}</p>
<div className="flex flex-wrap gap-2">
<span className="px-2 py-1 bg-kurdish-yellow/30 text-kurdish-yellow text-xs rounded-full">
{pallet.extrinsics.length} Extrinsics
</span>
<span className="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">
{pallet.storage.length} Storage Items
</span>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Modal */}
{selectedPallet && (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"
onClick={() => setSelectedPallet(null)}
>
<div
className="bg-gray-900 rounded-xl border border-gray-700 max-w-2xl w-full max-h-[80vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-2xl font-bold text-white">{selectedPallet.name}</h3>
<button
onClick={() => setSelectedPallet(null)}
className="text-gray-400 hover:text-white"
>
</button>
</div>
<div className="space-y-6">
<div>
<h4 className="text-lg font-semibold text-purple-400 mb-3">Extrinsics</h4>
<div className="space-y-2">
{selectedPallet.extrinsics.map((ext) => (
<div key={ext} className="flex items-center p-3 bg-gray-800/50 rounded-lg">
<Code className="w-4 h-4 text-cyan-400 mr-3" />
<code className="text-gray-300 font-mono text-sm">{ext}()</code>
</div>
))}
</div>
</div>
<div>
<h4 className="text-lg font-semibold text-cyan-400 mb-3">Storage Items</h4>
<div className="space-y-2">
{selectedPallet.storage.map((item) => (
<div key={item} className="flex items-center p-3 bg-gray-800/50 rounded-lg">
<Database className="w-4 h-4 text-purple-400 mr-3" />
<code className="text-gray-300 font-mono text-sm">{item}</code>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
)}
</section>
);
};
export default PalletsGrid;
+34
View File
@@ -0,0 +1,34 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { Loader2 } from 'lucide-react';
interface ProtectedRouteProps {
children: React.ReactNode;
requireAdmin?: boolean;
}
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children,
requireAdmin = false
}) => {
const { user, loading, isAdmin } = useAuth();
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900">
<Loader2 className="w-8 h-8 animate-spin text-green-500" />
</div>
);
}
if (!user) {
return <Navigate to="/login" replace />;
}
if (requireAdmin && !isAdmin) {
return <Navigate to="/" replace />;
}
return <>{children}</>;
};
+231
View File
@@ -0,0 +1,231 @@
import React, { useState } from 'react';
import { Gift, Calendar, Users, Timer, DollarSign } from 'lucide-react';
const RewardDistribution: React.FC = () => {
const [currentEpoch, setCurrentEpoch] = useState(1);
const [trustScoreInput, setTrustScoreInput] = useState(500);
const [totalParticipants, setTotalParticipants] = useState(1000);
const [totalTrustScore, setTotalTrustScore] = useState(500000);
const epochRewardPool = 1000000; // 1M PEZ per epoch
const parliamentaryAllocation = epochRewardPool * 0.1; // 10% for NFT holders
const trustScorePool = epochRewardPool * 0.9; // 90% for trust score rewards
const rewardPerTrustPoint = trustScorePool / totalTrustScore;
const userReward = trustScoreInput * rewardPerTrustPoint;
const nftRewardPerHolder = parliamentaryAllocation / 201;
const epochPhases = [
{ name: 'Active', duration: '30 days', blocks: 432000, status: 'current' },
{ name: 'Claim Period', duration: '7 days', blocks: 100800, status: 'upcoming' },
{ name: 'Closed', duration: 'Permanent', blocks: 0, status: 'final' }
];
return (
<section className="py-20 bg-gray-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
Reward Distribution System
</h2>
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
Monthly epoch-based rewards distributed by trust score and NFT holdings
</p>
</div>
<div className="mb-8">
<img
src="https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760315341305_1a772368.webp"
alt="Trust Score Network"
className="w-full h-64 object-cover rounded-xl opacity-80"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Epoch Timeline */}
<div className="lg:col-span-2 bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
<div className="flex items-center mb-6">
<Calendar className="w-6 h-6 text-purple-400 mr-3" />
<h3 className="text-xl font-semibold text-white">Epoch Timeline</h3>
</div>
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<span className="text-gray-400">Current Epoch</span>
<span className="text-2xl font-bold text-white">#{currentEpoch}</span>
</div>
<input
type="range"
min="1"
max="12"
value={currentEpoch}
onChange={(e) => setCurrentEpoch(parseInt(e.target.value))}
className="w-full"
/>
</div>
<div className="space-y-4">
{epochPhases.map((phase, index) => (
<div key={phase.name} className="relative">
<div className={`p-4 rounded-lg border ${
phase.status === 'current'
? 'bg-kurdish-green/20 border-kurdish-green/50'
: 'bg-gray-900/50 border-gray-700'
}`}>
<div className="flex items-center justify-between mb-2">
<h4 className={`font-semibold ${
phase.status === 'current' ? 'text-purple-400' : 'text-gray-300'
}`}>
{phase.name}
</h4>
<div className="flex items-center text-sm">
<Timer className="w-4 h-4 mr-1 text-gray-400" />
<span className="text-gray-400">{phase.duration}</span>
</div>
</div>
{phase.blocks > 0 && (
<div className="text-sm text-gray-500">
{phase.blocks.toLocaleString()} blocks
</div>
)}
</div>
{index < epochPhases.length - 1 && (
<div className="absolute left-8 top-full h-4 w-0.5 bg-gray-700"></div>
)}
</div>
))}
</div>
<div className="mt-6 grid grid-cols-2 gap-4">
<div className="p-4 bg-gray-900/50 rounded-lg">
<div className="text-gray-400 text-sm mb-1">Epoch Start Block</div>
<div className="text-white font-semibold">
#{((currentEpoch - 1) * 432000).toLocaleString()}
</div>
</div>
<div className="p-4 bg-gray-900/50 rounded-lg">
<div className="text-gray-400 text-sm mb-1">Claim Deadline Block</div>
<div className="text-cyan-400 font-semibold">
#{((currentEpoch * 432000) + 100800).toLocaleString()}
</div>
</div>
</div>
</div>
{/* Reward Pool Info */}
<div className="space-y-6">
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
<div className="flex items-center mb-4">
<Gift className="w-6 h-6 text-cyan-400 mr-3" />
<h3 className="text-lg font-semibold text-white">Epoch Pool</h3>
</div>
<div className="text-3xl font-bold text-white mb-4">
{epochRewardPool.toLocaleString()} PEZ
</div>
<div className="space-y-3">
<div className="flex justify-between p-3 bg-gray-800/50 rounded-lg">
<span className="text-gray-400">Trust Score Pool</span>
<span className="text-cyan-400 font-semibold">90%</span>
</div>
<div className="flex justify-between p-3 bg-gray-800/50 rounded-lg">
<span className="text-gray-400">Parliamentary NFTs</span>
<span className="text-purple-400 font-semibold">10%</span>
</div>
</div>
</div>
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
<div className="flex items-center mb-4">
<Users className="w-6 h-6 text-purple-400 mr-3" />
<h3 className="text-lg font-semibold text-white">NFT Rewards</h3>
</div>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-gray-400">Total NFTs</span>
<span className="text-white">201</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">Per NFT Reward</span>
<span className="text-purple-400 font-semibold">
{Math.floor(nftRewardPerHolder).toLocaleString()} PEZ
</span>
</div>
<div className="p-3 bg-kurdish-red/20 rounded-lg border border-kurdish-red/30">
<div className="text-xs text-purple-400 mb-1">Auto-distributed</div>
<div className="text-sm text-gray-300">No claim required</div>
</div>
</div>
</div>
</div>
</div>
{/* Reward Calculator */}
<div className="mt-8 bg-gradient-to-br from-purple-900/20 to-cyan-900/20 backdrop-blur-sm rounded-xl border border-purple-500/30 p-6">
<h3 className="text-xl font-semibold text-white mb-6 flex items-center">
<DollarSign className="w-6 h-6 text-cyan-400 mr-3" />
Reward Calculator
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="text-gray-400 text-sm block mb-2">Your Trust Score</label>
<input
type="number"
value={trustScoreInput}
onChange={(e) => setTrustScoreInput(parseInt(e.target.value) || 0)}
className="w-full px-4 py-2 bg-gray-800 text-white rounded-lg border border-gray-700 focus:border-cyan-500 focus:outline-none"
/>
</div>
<div>
<label className="text-gray-400 text-sm block mb-2">Total Participants</label>
<input
type="number"
value={totalParticipants}
onChange={(e) => setTotalParticipants(parseInt(e.target.value) || 1)}
className="w-full px-4 py-2 bg-gray-800 text-white rounded-lg border border-gray-700 focus:border-cyan-500 focus:outline-none"
/>
</div>
<div>
<label className="text-gray-400 text-sm block mb-2">Total Trust Score</label>
<input
type="number"
value={totalTrustScore}
onChange={(e) => setTotalTrustScore(parseInt(e.target.value) || 1)}
className="w-full px-4 py-2 bg-gray-800 text-white rounded-lg border border-gray-700 focus:border-cyan-500 focus:outline-none"
/>
</div>
</div>
<div className="mt-6 p-4 bg-gray-900/50 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center">
<div className="text-gray-400 text-sm mb-1">Reward per Trust Point</div>
<div className="text-xl font-semibold text-cyan-400">
{rewardPerTrustPoint.toFixed(4)} PEZ
</div>
</div>
<div className="text-center">
<div className="text-gray-400 text-sm mb-1">Your Share</div>
<div className="text-xl font-semibold text-purple-400">
{((trustScoreInput / totalTrustScore) * 100).toFixed(3)}%
</div>
</div>
<div className="text-center">
<div className="text-gray-400 text-sm mb-1">Estimated Reward</div>
<div className="text-2xl font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
{Math.floor(userReward).toLocaleString()} PEZ
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default RewardDistribution;
+112
View File
@@ -0,0 +1,112 @@
import React from 'react';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import { Users } from 'lucide-react';
interface TeamMember {
name: string;
role: string;
description: string;
image: string;
}
const TeamSection: React.FC = () => {
const teamMembers: TeamMember[] = [
{
name: "Satoshi Qazi Muhammed",
role: "Chief Architect",
description: "Blockchain visionary and protocol designer",
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358016604_9ae228b4.webp"
},
{
name: "Abdurrahman Qasimlo",
role: "Governance Lead",
description: "Democratic systems and consensus mechanisms",
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358018357_f19e128d.webp"
},
{
name: "Abdusselam Barzani",
role: "Protocol Engineer",
description: "Core protocol development and optimization",
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358020150_1ea35457.webp"
},
{
name: "Ihsan Nuri",
role: "Security Advisor",
description: "Cryptography and network security expert",
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358021872_362f1214.webp"
},
{
name: "Seyh Said",
role: "Community Director",
description: "Ecosystem growth and community relations",
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358023648_4bb8f4c7.webp"
},
{
name: "Seyyid Riza",
role: "Treasury Manager",
description: "Economic models and treasury operations",
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358025533_d9df77a9.webp"
},
{
name: "Beritan",
role: "Developer Relations",
description: "Technical documentation and developer support",
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358027281_9254657a.webp"
},
{
name: "Mashuk Xaznevi",
role: "Research Lead",
description: "Blockchain research and innovation",
image: "https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760358029000_3ffc04bc.webp"
}
];
return (
<section className="py-16 bg-gradient-to-b from-gray-900 to-black">
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<Badge className="mb-4 bg-kurdish-green/20 text-kurdish-green border-kurdish-green/30">
<Users className="w-4 h-4 mr-2" />
Our Team
</Badge>
<h2 className="text-4xl font-bold text-white mb-4">
Meet the Visionaries
</h2>
<p className="text-gray-400 max-w-2xl mx-auto">
A dedicated team of blockchain experts and governance specialists building the future of decentralized democracy
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{teamMembers.map((member, index) => (
<Card key={index} className="bg-gray-900/50 border-gray-800 hover:border-kurdish-green/50 transition-all duration-300 group">
<CardContent className="p-6">
<div className="flex flex-col items-center text-center">
<div className="w-32 h-32 rounded-full bg-gradient-to-br from-kurdish-green via-kurdish-red to-kurdish-yellow p-1 mb-4">
<img
src={member.image}
alt={member.name}
className="w-full h-full rounded-full object-cover"
/>
</div>
<h3 className="text-xl font-semibold text-white mb-1 group-hover:text-kurdish-green transition-colors">
{member.name}
</h3>
<Badge className="mb-3 bg-kurdish-red/20 text-kurdish-red border-kurdish-red/30">
{member.role}
</Badge>
<p className="text-gray-400 text-sm">
{member.description}
</p>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</section>
);
};
export default TeamSection;
+234
View File
@@ -0,0 +1,234 @@
import React, { useState } from 'react';
import { ArrowDownUp, Settings, Info, TrendingUp, Clock } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
const TokenSwap = () => {
const [fromToken, setFromToken] = useState('PEZ');
const [toToken, setToToken] = useState('HEZ');
const [fromAmount, setFromAmount] = useState('');
const [slippage, setSlippage] = useState('0.5');
const [showSettings, setShowSettings] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [isSwapping, setIsSwapping] = useState(false);
const exchangeRate = fromToken === 'PEZ' ? 2.5 : 0.4;
const toAmount = fromAmount ? (parseFloat(fromAmount) * exchangeRate).toFixed(4) : '';
const handleSwap = () => {
setFromToken(toToken);
setToToken(fromToken);
};
const handleConfirmSwap = () => {
setIsSwapping(true);
setTimeout(() => {
setIsSwapping(false);
setShowConfirm(false);
setFromAmount('');
}, 2000);
};
const liquidityData = [
{ pool: 'PEZ/HEZ', tvl: '2.5M', apr: '24.5%', volume: '850K' },
{ pool: 'PEZ/USDT', tvl: '1.8M', apr: '18.2%', volume: '620K' },
{ pool: 'HEZ/USDT', tvl: '1.2M', apr: '21.8%', volume: '480K' }
];
const txHistory = [
{ from: 'PEZ', to: 'HEZ', amount: '1000', rate: '2.48', time: '2 min ago' },
{ from: 'HEZ', to: 'PEZ', amount: '500', rate: '0.41', time: '5 min ago' },
{ from: 'PEZ', to: 'HEZ', amount: '2500', rate: '2.51', time: '12 min ago' }
];
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<Card className="p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold">Token Swap</h2>
<Button variant="ghost" size="icon" onClick={() => setShowSettings(true)}>
<Settings className="h-5 w-5" />
</Button>
</div>
<div className="space-y-4">
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex justify-between mb-2">
<span className="text-sm text-gray-600">From</span>
<span className="text-sm text-gray-600">Balance: 10,000</span>
</div>
<div className="flex gap-3">
<Input
type="number"
value={fromAmount}
onChange={(e) => setFromAmount(e.target.value)}
placeholder="0.0"
className="text-2xl font-bold border-0 bg-transparent"
/>
<Button variant="outline" className="min-w-[100px]">
{fromToken === 'PEZ' ? '🟣 PEZ' : '🟡 HEZ'}
</Button>
</div>
</div>
<div className="flex justify-center">
<Button
variant="ghost"
size="icon"
onClick={handleSwap}
className="rounded-full bg-white border-2"
>
<ArrowDownUp className="h-5 w-5" />
</Button>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex justify-between mb-2">
<span className="text-sm text-gray-600">To</span>
<span className="text-sm text-gray-600">Balance: 5,000</span>
</div>
<div className="flex gap-3">
<Input
type="text"
value={toAmount}
readOnly
placeholder="0.0"
className="text-2xl font-bold border-0 bg-transparent"
/>
<Button variant="outline" className="min-w-[100px]">
{toToken === 'PEZ' ? '🟣 PEZ' : '🟡 HEZ'}
</Button>
</div>
</div>
<div className="bg-blue-50 rounded-lg p-3">
<div className="flex justify-between text-sm">
<span>Exchange Rate</span>
<span className="font-semibold">1 {fromToken} = {exchangeRate} {toToken}</span>
</div>
<div className="flex justify-between text-sm mt-1">
<span>Slippage Tolerance</span>
<span className="font-semibold">{slippage}%</span>
</div>
</div>
<Button
className="w-full h-12 text-lg"
onClick={() => setShowConfirm(true)}
disabled={!fromAmount || parseFloat(fromAmount) <= 0}
>
Swap Tokens
</Button>
</div>
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Liquidity Pools
</h3>
<div className="space-y-3">
{liquidityData.map((pool, idx) => (
<div key={idx} className="flex justify-between items-center p-3 bg-gray-50 rounded-lg">
<div>
<div className="font-semibold">{pool.pool}</div>
<div className="text-sm text-gray-600">TVL: ${pool.tvl}</div>
</div>
<div className="text-right">
<div className="text-green-600 font-semibold">{pool.apr} APR</div>
<div className="text-sm text-gray-600">Vol: ${pool.volume}</div>
</div>
</div>
))}
</div>
</Card>
</div>
<div>
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Clock className="h-5 w-5" />
Recent Transactions
</h3>
<div className="space-y-3">
{txHistory.map((tx, idx) => (
<div key={idx} className="p-3 bg-gray-50 rounded-lg">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold">{tx.amount} {tx.from}</span>
<ArrowDownUp className="h-4 w-4 text-gray-400" />
</div>
<div className="flex justify-between text-sm text-gray-600">
<span>Rate: {tx.rate}</span>
<span>{tx.time}</span>
</div>
</div>
))}
</div>
</Card>
</div>
<Dialog open={showSettings} onOpenChange={setShowSettings}>
<DialogContent>
<DialogHeader>
<DialogTitle>Swap Settings</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="text-sm font-medium">Slippage Tolerance</label>
<div className="flex gap-2 mt-2">
{['0.1', '0.5', '1.0'].map(val => (
<Button
key={val}
variant={slippage === val ? 'default' : 'outline'}
onClick={() => setSlippage(val)}
className="flex-1"
>
{val}%
</Button>
))}
<Input
type="number"
value={slippage}
onChange={(e) => setSlippage(e.target.value)}
className="w-20"
/>
</div>
</div>
</div>
</DialogContent>
</Dialog>
<Dialog open={showConfirm} onOpenChange={setShowConfirm}>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Swap</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="p-4 bg-gray-50 rounded-lg">
<div className="flex justify-between mb-2">
<span>You Pay</span>
<span className="font-bold">{fromAmount} {fromToken}</span>
</div>
<div className="flex justify-between">
<span>You Receive</span>
<span className="font-bold">{toAmount} {toToken}</span>
</div>
</div>
<Button
className="w-full"
onClick={handleConfirmSwap}
disabled={isSwapping}
>
{isSwapping ? 'Swapping...' : 'Confirm Swap'}
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
};
export default TokenSwap;
+179
View File
@@ -0,0 +1,179 @@
import React, { useState, useEffect } from 'react';
import { PieChart, Clock, TrendingDown, Coins, ArrowRightLeft } from 'lucide-react';
const TokenomicsSection: React.FC = () => {
const [selectedToken, setSelectedToken] = useState<'PEZ' | 'HEZ'>('PEZ');
const [monthsPassed, setMonthsPassed] = useState(0);
const [currentRelease, setCurrentRelease] = useState(0);
const halvingPeriod = Math.floor(monthsPassed / 48);
const monthsUntilNextHalving = 48 - (monthsPassed % 48);
useEffect(() => {
const baseAmount = selectedToken === 'PEZ' ? 74218750 : 37109375;
const release = baseAmount / Math.pow(2, halvingPeriod);
setCurrentRelease(release);
}, [monthsPassed, halvingPeriod, selectedToken]);
const pezDistribution = [
{ name: 'Treasury', percentage: 96.25, amount: 4812500000, color: 'from-purple-500 to-purple-600' },
{ name: 'Presale', percentage: 1.875, amount: 93750000, color: 'from-cyan-500 to-cyan-600' },
{ name: 'Founder', percentage: 1.875, amount: 93750000, color: 'from-teal-500 to-teal-600' }
];
const hezDistribution = [
{ name: 'Staking Rewards', percentage: 40, amount: 1000000000, color: 'from-yellow-500 to-orange-600' },
{ name: 'Governance', percentage: 30, amount: 750000000, color: 'from-green-500 to-emerald-600' },
{ name: 'Ecosystem', percentage: 20, amount: 500000000, color: 'from-blue-500 to-indigo-600' },
{ name: 'Team', percentage: 10, amount: 250000000, color: 'from-red-500 to-pink-600' }
];
const distribution = selectedToken === 'PEZ' ? pezDistribution : hezDistribution;
const totalSupply = selectedToken === 'PEZ' ? 5000000000 : 2500000000;
const tokenColor = selectedToken === 'PEZ' ? 'purple' : 'yellow';
return (
<section id="tokenomics" className="py-20 bg-gray-900/50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-yellow-400 bg-clip-text text-transparent">
Dual Token Ecosystem
</h2>
<p className="text-gray-400 text-lg max-w-2xl mx-auto mb-6">
PEZ & HEZ tokens working together for governance and utility
</p>
{/* Token Selector */}
<div className="inline-flex bg-gray-950/50 rounded-lg p-1 border border-gray-800">
<button
onClick={() => setSelectedToken('PEZ')}
className={`px-6 py-2 rounded-md font-semibold transition-all ${
selectedToken === 'PEZ'
? 'bg-purple-600 text-white'
: 'text-gray-400 hover:text-white'
}`}
>
PEZ Token
</button>
<button
onClick={() => setSelectedToken('HEZ')}
className={`px-6 py-2 rounded-md font-semibold transition-all ${
selectedToken === 'HEZ'
? 'bg-yellow-600 text-white'
: 'text-gray-400 hover:text-white'
}`}
>
HEZ Token
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Distribution Chart */}
<div className="bg-gray-950/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
<div className="flex items-center mb-6">
<PieChart className={`w-6 h-6 text-${tokenColor}-400 mr-3`} />
<h3 className="text-xl font-semibold text-white">{selectedToken} Distribution</h3>
</div>
<div className="flex justify-center mb-6">
<div className={`w-48 h-48 rounded-full bg-gradient-to-br from-${tokenColor}-500 to-${tokenColor}-700 flex items-center justify-center`}>
<span className="text-white text-3xl font-bold">{selectedToken}</span>
</div>
</div>
<div className="space-y-3">
{distribution.map((item) => (
<div key={item.name} className="flex items-center justify-between p-3 bg-gray-900/50 rounded-lg">
<div className="flex items-center">
<div className={`w-3 h-3 rounded-full bg-gradient-to-r ${item.color} mr-3`}></div>
<span className="text-gray-300">{item.name}</span>
</div>
<div className="text-right">
<div className="text-white font-semibold">{item.percentage}%</div>
<div className="text-gray-500 text-sm">{item.amount.toLocaleString()} {selectedToken}</div>
</div>
</div>
))}
</div>
<div className={`mt-6 p-4 bg-${tokenColor}-500/20 rounded-lg border border-${tokenColor}-500/30`}>
<div className="flex items-center justify-between">
<span className={`text-${tokenColor}-400`}>Total Supply</span>
<span className="text-white font-bold">{totalSupply.toLocaleString()} {selectedToken}</span>
</div>
</div>
</div>
{/* Token Features */}
<div className="bg-gray-950/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
<div className="flex items-center mb-6">
<ArrowRightLeft className={`w-6 h-6 text-${tokenColor}-400 mr-3`} />
<h3 className="text-xl font-semibold text-white">{selectedToken} Features</h3>
</div>
{selectedToken === 'PEZ' ? (
<div className="space-y-4">
<div className="p-4 bg-purple-900/20 rounded-lg border border-purple-500/30">
<h4 className="text-purple-400 font-semibold mb-2">Governance Token</h4>
<p className="text-gray-300 text-sm">Vote on proposals and participate in DAO decisions</p>
</div>
<div className="p-4 bg-purple-900/20 rounded-lg border border-purple-500/30">
<h4 className="text-purple-400 font-semibold mb-2">Staking Rewards</h4>
<p className="text-gray-300 text-sm">Earn HEZ tokens by staking PEZ</p>
</div>
<div className="p-4 bg-purple-900/20 rounded-lg border border-purple-500/30">
<h4 className="text-purple-400 font-semibold mb-2">Treasury Access</h4>
<p className="text-gray-300 text-sm">Propose and vote on treasury fund allocation</p>
</div>
<div className="p-4 bg-purple-900/20 rounded-lg border border-purple-500/30">
<h4 className="text-purple-400 font-semibold mb-2">Deflationary</h4>
<p className="text-gray-300 text-sm">Synthetic halving every 48 months</p>
</div>
</div>
) : (
<div className="space-y-4">
<div className="p-4 bg-yellow-900/20 rounded-lg border border-yellow-500/30">
<h4 className="text-yellow-400 font-semibold mb-2">Utility Token</h4>
<p className="text-gray-300 text-sm">Used for platform transactions and services</p>
</div>
<div className="p-4 bg-yellow-900/20 rounded-lg border border-yellow-500/30">
<h4 className="text-yellow-400 font-semibold mb-2">P2P Trading</h4>
<p className="text-gray-300 text-sm">Primary currency for peer-to-peer marketplace</p>
</div>
<div className="p-4 bg-yellow-900/20 rounded-lg border border-yellow-500/30">
<h4 className="text-yellow-400 font-semibold mb-2">Fee Discounts</h4>
<p className="text-gray-300 text-sm">Reduced platform fees when using HEZ</p>
</div>
<div className="p-4 bg-yellow-900/20 rounded-lg border border-yellow-500/30">
<h4 className="text-yellow-400 font-semibold mb-2">Reward Distribution</h4>
<p className="text-gray-300 text-sm">Earned through staking and participation</p>
</div>
</div>
)}
<div className="mt-6 p-4 bg-gradient-to-r from-purple-900/20 to-yellow-900/20 rounded-lg border border-gray-700">
<h4 className="text-white font-semibold mb-3">Token Synergy</h4>
<div className="space-y-2 text-sm">
<div className="flex items-center text-gray-300">
<span className="w-2 h-2 bg-purple-400 rounded-full mr-2"></span>
Stake PEZ Earn HEZ rewards
</div>
<div className="flex items-center text-gray-300">
<span className="w-2 h-2 bg-yellow-400 rounded-full mr-2"></span>
Use HEZ Boost governance power
</div>
<div className="flex items-center text-gray-300">
<span className="w-2 h-2 bg-green-400 rounded-full mr-2"></span>
Hold both Maximum platform benefits
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default TokenomicsSection;
+243
View File
@@ -0,0 +1,243 @@
import React, { useState, useEffect } from 'react';
import { Calculator, TrendingUp, Users, BookOpen, Award } from 'lucide-react';
const TrustScoreCalculator: React.FC = () => {
const [stakingScore, setStakingScore] = useState(50);
const [stakingMonths, setStakingMonths] = useState(6);
const [referralCount, setReferralCount] = useState(5);
const [perwerdeScore, setPerwerdeScore] = useState(30);
const [tikiScore, setTikiScore] = useState(40);
const [finalScore, setFinalScore] = useState(0);
// Calculate staking multiplier based on months
const getStakingMultiplier = (months: number) => {
if (months < 1) return 1.0;
if (months < 3) return 1.2;
if (months < 6) return 1.4;
if (months < 12) return 1.7;
return 2.0;
};
// Calculate referral score
const getReferralScore = (count: number) => {
if (count === 0) return 0;
if (count <= 5) return count * 4;
if (count <= 20) return 20 + ((count - 5) * 2);
return 50;
};
useEffect(() => {
const multiplier = getStakingMultiplier(stakingMonths);
const adjustedStaking = Math.min(stakingScore * multiplier, 100);
const adjustedReferral = getReferralScore(referralCount);
const weightedSum =
adjustedStaking * 100 +
adjustedReferral * 300 +
perwerdeScore * 300 +
tikiScore * 300;
const score = (adjustedStaking * weightedSum) / 1000;
setFinalScore(Math.round(score));
}, [stakingScore, stakingMonths, referralCount, perwerdeScore, tikiScore]);
return (
<section className="py-20 bg-gray-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
Trust Score Calculator
</h2>
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
Simulate your trust score based on staking, referrals, education, and roles
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Calculator Inputs */}
<div className="space-y-6">
{/* Staking Score */}
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
<div className="flex items-center mb-4">
<TrendingUp className="w-5 h-5 text-purple-400 mr-3" />
<h3 className="text-lg font-semibold text-white">Staking Score</h3>
</div>
<div className="space-y-4">
<div>
<label className="text-gray-400 text-sm">Base Staking Score (0-100)</label>
<input
type="range"
min="0"
max="100"
value={stakingScore}
onChange={(e) => setStakingScore(parseInt(e.target.value))}
className="w-full mt-2"
/>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>0</span>
<span>{stakingScore}</span>
<span>100</span>
</div>
</div>
<div>
<label className="text-gray-400 text-sm">Staking Duration (Months)</label>
<input
type="range"
min="0"
max="24"
value={stakingMonths}
onChange={(e) => setStakingMonths(parseInt(e.target.value))}
className="w-full mt-2"
/>
<div className="flex justify-between items-center mt-2">
<span className="text-cyan-400">{stakingMonths} months</span>
<span className="text-purple-400">×{getStakingMultiplier(stakingMonths).toFixed(1)} multiplier</span>
</div>
</div>
</div>
</div>
{/* Referral Score */}
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
<div className="flex items-center mb-4">
<Users className="w-5 h-5 text-cyan-400 mr-3" />
<h3 className="text-lg font-semibold text-white">Referral Score</h3>
</div>
<div>
<label className="text-gray-400 text-sm">Number of Referrals</label>
<input
type="number"
min="0"
max="50"
value={referralCount}
onChange={(e) => setReferralCount(parseInt(e.target.value) || 0)}
className="w-full mt-2 px-4 py-2 bg-gray-800 text-white rounded-lg border border-gray-700 focus:border-cyan-500 focus:outline-none"
/>
<div className="mt-2 text-sm text-cyan-400">
Score: {getReferralScore(referralCount)} points
</div>
</div>
</div>
{/* Other Scores */}
<div className="grid grid-cols-2 gap-4">
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
<div className="flex items-center mb-4">
<BookOpen className="w-5 h-5 text-teal-400 mr-3" />
<h3 className="text-sm font-semibold text-white">Perwerde Score</h3>
</div>
<input
type="range"
min="0"
max="100"
value={perwerdeScore}
onChange={(e) => setPerwerdeScore(parseInt(e.target.value))}
className="w-full"
/>
<div className="text-center mt-2 text-teal-400">{perwerdeScore}</div>
</div>
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
<div className="flex items-center mb-4">
<Award className="w-5 h-5 text-purple-400 mr-3" />
<h3 className="text-sm font-semibold text-white">Tiki Score</h3>
</div>
<input
type="range"
min="0"
max="100"
value={tikiScore}
onChange={(e) => setTikiScore(parseInt(e.target.value))}
className="w-full"
/>
<div className="text-center mt-2 text-purple-400">{tikiScore}</div>
</div>
</div>
</div>
{/* Results and Formula */}
<div className="space-y-6">
{/* Final Score */}
<div className="bg-gradient-to-br from-purple-900/30 to-cyan-900/30 backdrop-blur-sm rounded-xl border border-purple-500/50 p-8 text-center">
<Calculator className="w-12 h-12 text-cyan-400 mx-auto mb-4" />
<h3 className="text-2xl font-semibold text-white mb-2">Final Trust Score</h3>
<div className="text-6xl font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
{finalScore}
</div>
<div className="mt-4 text-gray-400">
Out of theoretical maximum
</div>
</div>
{/* Formula Breakdown */}
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
<h3 className="text-lg font-semibold text-white mb-4">Formula Breakdown</h3>
<div className="bg-gray-950/50 rounded-lg p-4 font-mono text-sm">
<div className="text-purple-400 mb-2">
weighted_sum =
</div>
<div className="text-gray-300 ml-4">
staking × 100 +
</div>
<div className="text-gray-300 ml-4">
referral × 300 +
</div>
<div className="text-gray-300 ml-4">
perwerde × 300 +
</div>
<div className="text-gray-300 ml-4 mb-2">
tiki × 300
</div>
<div className="text-cyan-400">
final_score = staking × weighted_sum / 1000
</div>
</div>
<div className="mt-4 space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Staking Component:</span>
<span className="text-purple-400">{Math.round(stakingScore * getStakingMultiplier(stakingMonths))} × 100</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Referral Component:</span>
<span className="text-cyan-400">{getReferralScore(referralCount)} × 300</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Perwerde Component:</span>
<span className="text-teal-400">{perwerdeScore} × 300</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Tiki Component:</span>
<span className="text-purple-400">{tikiScore} × 300</span>
</div>
</div>
</div>
{/* Score Impact */}
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-800 p-6">
<h3 className="text-lg font-semibold text-white mb-4">Score Impact</h3>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg">
<span className="text-gray-400">Monthly Rewards Eligibility</span>
<span className={`px-3 py-1 rounded-full text-sm ${finalScore > 100 ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400'}`}>
{finalScore > 100 ? 'Eligible' : 'Not Eligible'}
</span>
</div>
<div className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg">
<span className="text-gray-400">Governance Voting Weight</span>
<span className="text-cyan-400 font-semibold">{Math.min(Math.floor(finalScore / 100), 10)}x</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default TrustScoreCalculator;
+219
View File
@@ -0,0 +1,219 @@
import { useState } from 'react';
import { supabase } from '@/lib/supabase';
import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Shield, Copy, Check, AlertCircle } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
export function TwoFactorSetup() {
const { user } = useAuth();
const { toast } = useToast();
const [isEnabled, setIsEnabled] = useState(false);
const [secret, setSecret] = useState('');
const [backupCodes, setBackupCodes] = useState<string[]>([]);
const [verificationCode, setVerificationCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [showSetup, setShowSetup] = useState(false);
const [copiedCodes, setCopiedCodes] = useState(false);
const handleSetup = async () => {
setIsLoading(true);
try {
const { data, error } = await supabase.functions.invoke('two-factor-auth', {
body: { action: 'setup', userId: user?.id }
});
if (error) throw error;
setSecret(data.secret);
setBackupCodes(data.backupCodes);
setShowSetup(true);
toast({
title: '2FA Setup Started',
description: 'Scan the QR code with your authenticator app',
});
} catch (error) {
toast({
title: 'Setup Failed',
description: error.message,
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
};
const handleEnable = async () => {
if (!verificationCode) {
toast({
title: 'Error',
description: 'Please enter verification code',
variant: 'destructive',
});
return;
}
setIsLoading(true);
try {
const { data, error } = await supabase.functions.invoke('two-factor-auth', {
body: {
action: 'enable',
userId: user?.id,
code: verificationCode
}
});
if (error) throw error;
setIsEnabled(true);
setShowSetup(false);
toast({
title: '2FA Enabled',
description: 'Your account is now protected with two-factor authentication',
});
} catch (error) {
toast({
title: 'Verification Failed',
description: error.message,
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
};
const handleDisable = async () => {
setIsLoading(true);
try {
const { data, error } = await supabase.functions.invoke('two-factor-auth', {
body: { action: 'disable', userId: user?.id }
});
if (error) throw error;
setIsEnabled(false);
setSecret('');
setBackupCodes([]);
toast({
title: '2FA Disabled',
description: 'Two-factor authentication has been disabled',
});
} catch (error) {
toast({
title: 'Error',
description: error.message,
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
};
const copyBackupCodes = () => {
navigator.clipboard.writeText(backupCodes.join('\n'));
setCopiedCodes(true);
setTimeout(() => setCopiedCodes(false), 2000);
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Two-Factor Authentication
</CardTitle>
<CardDescription>
Add an extra layer of security to your account
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{!isEnabled && !showSetup && (
<div className="space-y-4">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Two-factor authentication adds an extra layer of security by requiring a code from your authenticator app
</AlertDescription>
</Alert>
<Button onClick={handleSetup} disabled={isLoading}>
Set Up Two-Factor Authentication
</Button>
</div>
)}
{showSetup && (
<div className="space-y-4">
<div className="p-4 border rounded-lg">
<p className="text-sm font-medium mb-2">1. Scan QR Code</p>
<p className="text-xs text-muted-foreground mb-4">
Use your authenticator app to scan this QR code or enter the secret manually
</p>
<div className="bg-muted p-2 rounded font-mono text-xs break-all">
{secret}
</div>
</div>
<div className="p-4 border rounded-lg">
<p className="text-sm font-medium mb-2">2. Save Backup Codes</p>
<p className="text-xs text-muted-foreground mb-4">
Store these codes in a safe place. You can use them to access your account if you lose your device.
</p>
<div className="bg-muted p-3 rounded space-y-1">
{backupCodes.map((code, i) => (
<div key={i} className="font-mono text-xs">{code}</div>
))}
</div>
<Button
variant="outline"
size="sm"
className="mt-2"
onClick={copyBackupCodes}
>
{copiedCodes ? <Check className="h-4 w-4 mr-2" /> : <Copy className="h-4 w-4 mr-2" />}
{copiedCodes ? 'Copied!' : 'Copy Codes'}
</Button>
</div>
<div className="p-4 border rounded-lg">
<p className="text-sm font-medium mb-2">3. Verify Setup</p>
<p className="text-xs text-muted-foreground mb-4">
Enter the 6-digit code from your authenticator app
</p>
<div className="flex gap-2">
<Input
placeholder="000000"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
maxLength={6}
/>
<Button onClick={handleEnable} disabled={isLoading}>
Enable 2FA
</Button>
</div>
</div>
</div>
)}
{isEnabled && (
<div className="space-y-4">
<Alert className="border-green-200 bg-green-50">
<Check className="h-4 w-4 text-green-600" />
<AlertDescription className="text-green-800">
Two-factor authentication is enabled for your account
</AlertDescription>
</Alert>
<Button variant="destructive" onClick={handleDisable} disabled={isLoading}>
Disable Two-Factor Authentication
</Button>
</div>
)}
</CardContent>
</Card>
);
}
+148
View File
@@ -0,0 +1,148 @@
import { useState } from 'react';
import { supabase } from '@/lib/supabase';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Shield, Loader2 } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
interface TwoFactorVerifyProps {
userId: string;
onSuccess: () => void;
onCancel?: () => void;
}
export function TwoFactorVerify({ userId, onSuccess, onCancel }: TwoFactorVerifyProps) {
const { toast } = useToast();
const [verificationCode, setVerificationCode] = useState('');
const [backupCode, setBackupCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleVerify = async (useBackup: boolean = false) => {
const code = useBackup ? backupCode : verificationCode;
if (!code) {
toast({
title: 'Error',
description: 'Please enter a code',
variant: 'destructive',
});
return;
}
setIsLoading(true);
try {
const { data, error } = await supabase.functions.invoke('two-factor-auth', {
body: {
action: 'verify',
userId,
code: useBackup ? undefined : code,
backupCode: useBackup ? code : undefined
}
});
if (error) throw error;
if (data.success) {
toast({
title: 'Verification Successful',
description: 'You have been authenticated',
});
onSuccess();
} else {
throw new Error(data.error || 'Verification failed');
}
} catch (error) {
toast({
title: 'Verification Failed',
description: error.message,
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
};
return (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Two-Factor Authentication
</CardTitle>
<CardDescription>
Enter your authentication code to continue
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="authenticator" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="authenticator">Authenticator App</TabsTrigger>
<TabsTrigger value="backup">Backup Code</TabsTrigger>
</TabsList>
<TabsContent value="authenticator" className="space-y-4">
<Alert>
<AlertDescription>
Enter the 6-digit code from your authenticator app
</AlertDescription>
</Alert>
<Input
placeholder="000000"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
maxLength={6}
className="text-center text-2xl font-mono"
/>
<div className="flex gap-2">
<Button
className="flex-1"
onClick={() => handleVerify(false)}
disabled={isLoading}
>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Verify
</Button>
{onCancel && (
<Button variant="outline" onClick={onCancel} disabled={isLoading}>
Cancel
</Button>
)}
</div>
</TabsContent>
<TabsContent value="backup" className="space-y-4">
<Alert>
<AlertDescription>
Enter one of your backup codes
</AlertDescription>
</Alert>
<Input
placeholder="Backup code"
value={backupCode}
onChange={(e) => setBackupCode(e.target.value)}
className="font-mono"
/>
<div className="flex gap-2">
<Button
className="flex-1"
onClick={() => handleVerify(true)}
disabled={isLoading}
>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Verify
</Button>
{onCancel && (
<Button variant="outline" onClick={onCancel} disabled={isLoading}>
Cancel
</Button>
)}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}
@@ -0,0 +1,280 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Switch } from '@/components/ui/switch';
import { CheckCircle, Info, TrendingUp, Users, Award } from 'lucide-react';
const DelegateProfile: React.FC = () => {
const { t } = useTranslation();
const [isDelegate, setIsDelegate] = useState(false);
const [profileData, setProfileData] = useState({
statement: '',
expertise: [],
commitments: '',
website: '',
twitter: '',
acceptingDelegations: true,
minDelegation: '100',
maxDelegation: '100000'
});
const expertiseOptions = [
'Treasury Management',
'Technical Development',
'Community Building',
'Governance Design',
'Security',
'Economics',
'Marketing',
'Legal'
];
const handleBecomeDelegate = () => {
setIsDelegate(true);
console.log('Becoming a delegate with:', profileData);
};
if (!isDelegate) {
return (
<Card className="border-green-200">
<CardHeader>
<CardTitle>{t('delegation.becomeDelegate')}</CardTitle>
<CardDescription>
{t('delegation.becomeDelegateDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<Alert className="border-blue-200 bg-blue-50">
<Info className="w-4 h-4" />
<AlertDescription>
{t('delegation.delegateRequirements')}
</AlertDescription>
</Alert>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardContent className="p-4 text-center">
<TrendingUp className="w-8 h-8 mx-auto mb-2 text-green-600" />
<h4 className="font-semibold mb-1">{t('delegation.buildReputation')}</h4>
<p className="text-sm text-gray-600">{t('delegation.buildReputationDesc')}</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<Users className="w-8 h-8 mx-auto mb-2 text-yellow-600" />
<h4 className="font-semibold mb-1">{t('delegation.earnTrust')}</h4>
<p className="text-sm text-gray-600">{t('delegation.earnTrustDesc')}</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<Award className="w-8 h-8 mx-auto mb-2 text-red-600" />
<h4 className="font-semibold mb-1">{t('delegation.getRewards')}</h4>
<p className="text-sm text-gray-600">{t('delegation.getRewardsDesc')}</p>
</CardContent>
</Card>
</div>
<div className="space-y-4">
<div>
<Label htmlFor="statement">{t('delegation.delegateStatement')}</Label>
<Textarea
id="statement"
placeholder={t('delegation.statementPlaceholder')}
value={profileData.statement}
onChange={(e) => setProfileData({...profileData, statement: e.target.value})}
rows={4}
/>
</div>
<div>
<Label>{t('delegation.expertise')}</Label>
<div className="flex flex-wrap gap-2 mt-2">
{expertiseOptions.map((option) => (
<label key={option} className="flex items-center gap-2">
<input
type="checkbox"
className="rounded"
onChange={(e) => {
if (e.target.checked) {
setProfileData({
...profileData,
expertise: [...profileData.expertise, option]
});
} else {
setProfileData({
...profileData,
expertise: profileData.expertise.filter(e => e !== option)
});
}
}}
/>
<span className="text-sm">{option}</span>
</label>
))}
</div>
</div>
<div>
<Label htmlFor="commitments">{t('delegation.commitments')}</Label>
<Textarea
id="commitments"
placeholder={t('delegation.commitmentsPlaceholder')}
value={profileData.commitments}
onChange={(e) => setProfileData({...profileData, commitments: e.target.value})}
rows={3}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="website">{t('delegation.website')}</Label>
<Input
id="website"
placeholder="https://..."
value={profileData.website}
onChange={(e) => setProfileData({...profileData, website: e.target.value})}
/>
</div>
<div>
<Label htmlFor="twitter">{t('delegation.twitter')}</Label>
<Input
id="twitter"
placeholder="@username"
value={profileData.twitter}
onChange={(e) => setProfileData({...profileData, twitter: e.target.value})}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="minDelegation">{t('delegation.minDelegation')}</Label>
<Input
id="minDelegation"
type="number"
placeholder="Min PZK"
value={profileData.minDelegation}
onChange={(e) => setProfileData({...profileData, minDelegation: e.target.value})}
/>
</div>
<div>
<Label htmlFor="maxDelegation">{t('delegation.maxDelegation')}</Label>
<Input
id="maxDelegation"
type="number"
placeholder="Max PZK"
value={profileData.maxDelegation}
onChange={(e) => setProfileData({...profileData, maxDelegation: e.target.value})}
/>
</div>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<Label htmlFor="accepting">{t('delegation.acceptingDelegations')}</Label>
<p className="text-sm text-gray-600">{t('delegation.acceptingDesc')}</p>
</div>
<Switch
id="accepting"
checked={profileData.acceptingDelegations}
onCheckedChange={(checked) =>
setProfileData({...profileData, acceptingDelegations: checked})
}
/>
</div>
</div>
<Button
onClick={handleBecomeDelegate}
className="w-full bg-green-600 hover:bg-green-700"
disabled={!profileData.statement || profileData.expertise.length === 0}
>
{t('delegation.activateDelegate')}
</Button>
</CardContent>
</Card>
);
}
return (
<Card className="border-green-200">
<CardHeader>
<CardTitle className="flex items-center gap-2">
{t('delegation.yourDelegateProfile')}
<Badge className="bg-green-100 text-green-800">
<CheckCircle className="w-3 h-3 mr-1" />
Active
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<Alert className="border-green-200 bg-green-50 mb-6">
<CheckCircle className="w-4 h-4" />
<AlertDescription>
{t('delegation.delegateActive')}
</AlertDescription>
</Alert>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-green-600">0</div>
<div className="text-sm text-gray-600">{t('delegation.delegators')}</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-yellow-600">0 PZK</div>
<div className="text-sm text-gray-600">{t('delegation.totalReceived')}</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-red-600">0%</div>
<div className="text-sm text-gray-600">{t('delegation.successRate')}</div>
</CardContent>
</Card>
</div>
<div className="space-y-4">
<div>
<h4 className="font-semibold mb-2">{t('delegation.yourStatement')}</h4>
<p className="text-gray-700">{profileData.statement}</p>
</div>
<div>
<h4 className="font-semibold mb-2">{t('delegation.yourExpertise')}</h4>
<div className="flex flex-wrap gap-2">
{profileData.expertise.map((exp) => (
<Badge key={exp} variant="secondary">{exp}</Badge>
))}
</div>
</div>
<div>
<h4 className="font-semibold mb-2">{t('delegation.delegationLimits')}</h4>
<p className="text-gray-700">
Min: {profileData.minDelegation} PZK | Max: {profileData.maxDelegation} PZK
</p>
</div>
</div>
<div className="flex gap-2 mt-6">
<Button variant="outline">
{t('delegation.editProfile')}
</Button>
<Button variant="outline">
{t('delegation.pauseDelegations')}
</Button>
</div>
</CardContent>
</Card>
);
};
export default DelegateProfile;
@@ -0,0 +1,323 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Users, TrendingUp, Shield, Clock, ChevronRight, Award } from 'lucide-react';
import DelegateProfile from './DelegateProfile';
const DelegationManager: React.FC = () => {
const { t } = useTranslation();
const [selectedDelegate, setSelectedDelegate] = useState<any>(null);
const [delegationAmount, setDelegationAmount] = useState('');
const [delegationPeriod, setDelegationPeriod] = useState('3months');
const delegates = [
{
id: 1,
name: 'Leyla Zana',
address: '0x1234...5678',
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330',
reputation: 9500,
successRate: 92,
totalDelegated: 125000,
activeProposals: 8,
categories: ['Treasury', 'Community'],
description: 'Focused on community development and treasury management',
performance: {
proposalsCreated: 45,
proposalsPassed: 41,
participationRate: 98
}
},
{
id: 2,
name: 'Mazlum Doğan',
address: '0x8765...4321',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d',
reputation: 8800,
successRate: 88,
totalDelegated: 98000,
activeProposals: 6,
categories: ['Technical', 'Governance'],
description: 'Technical expert specializing in protocol upgrades',
performance: {
proposalsCreated: 32,
proposalsPassed: 28,
participationRate: 95
}
},
{
id: 3,
name: 'Sakine Cansız',
address: '0x9876...1234',
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80',
reputation: 9200,
successRate: 90,
totalDelegated: 110000,
activeProposals: 7,
categories: ['Community', 'Governance'],
description: 'Community organizer with focus on inclusive governance',
performance: {
proposalsCreated: 38,
proposalsPassed: 34,
participationRate: 96
}
}
];
const myDelegations = [
{
id: 1,
delegate: 'Leyla Zana',
amount: 5000,
category: 'Treasury',
period: '3 months',
remaining: '45 days',
status: 'active'
},
{
id: 2,
delegate: 'Mazlum Doğan',
amount: 3000,
category: 'Technical',
period: '6 months',
remaining: '120 days',
status: 'active'
}
];
const handleDelegate = () => {
console.log('Delegating:', {
delegate: selectedDelegate,
amount: delegationAmount,
period: delegationPeriod
});
};
return (
<div className="container mx-auto px-4 py-8 max-w-7xl">
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">{t('delegation.title')}</h1>
<p className="text-gray-600">{t('delegation.description')}</p>
</div>
{/* Stats Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<Card className="border-green-200">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Users className="w-8 h-8 text-green-600" />
<div>
<div className="text-2xl font-bold">12</div>
<div className="text-sm text-gray-600">{t('delegation.activeDelegates')}</div>
</div>
</div>
</CardContent>
</Card>
<Card className="border-yellow-200">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<TrendingUp className="w-8 h-8 text-yellow-600" />
<div>
<div className="text-2xl font-bold">450K</div>
<div className="text-sm text-gray-600">{t('delegation.totalDelegated')}</div>
</div>
</div>
</CardContent>
</Card>
<Card className="border-red-200">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Shield className="w-8 h-8 text-red-600" />
<div>
<div className="text-2xl font-bold">89%</div>
<div className="text-sm text-gray-600">{t('delegation.avgSuccessRate')}</div>
</div>
</div>
</CardContent>
</Card>
<Card className="border-blue-200">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Clock className="w-8 h-8 text-blue-600" />
<div>
<div className="text-2xl font-bold">8K</div>
<div className="text-sm text-gray-600">{t('delegation.yourDelegated')}</div>
</div>
</div>
</CardContent>
</Card>
</div>
<Tabs defaultValue="explore" className="space-y-6">
<TabsList className="grid w-full grid-cols-3 bg-green-50">
<TabsTrigger value="explore">{t('delegation.explore')}</TabsTrigger>
<TabsTrigger value="my-delegations">{t('delegation.myDelegations')}</TabsTrigger>
<TabsTrigger value="delegate-profile">{t('delegation.becomeDelegate')}</TabsTrigger>
</TabsList>
<TabsContent value="explore">
<Card className="border-green-200">
<CardHeader>
<CardTitle>{t('delegation.topDelegates')}</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{delegates.map((delegate) => (
<div
key={delegate.id}
className="border rounded-lg p-4 hover:bg-gray-50 transition-colors cursor-pointer"
onClick={() => setSelectedDelegate(delegate)}
>
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
<img
src={delegate.avatar}
alt={delegate.name}
className="w-12 h-12 rounded-full"
/>
<div>
<h3 className="font-semibold flex items-center gap-2">
{delegate.name}
<Badge className="bg-green-100 text-green-800">
{delegate.successRate}% success
</Badge>
</h3>
<p className="text-sm text-gray-600 mb-2">{delegate.description}</p>
<div className="flex flex-wrap gap-2 mb-2">
{delegate.categories.map((cat) => (
<Badge key={cat} variant="secondary">{cat}</Badge>
))}
</div>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span className="flex items-center gap-1">
<TrendingUp className="w-3 h-3" />
{delegate.reputation} rep
</span>
<span className="flex items-center gap-1">
<Users className="w-3 h-3" />
{(delegate.totalDelegated / 1000).toFixed(0)}K delegated
</span>
<span className="flex items-center gap-1">
<Award className="w-3 h-3" />
{delegate.activeProposals} active
</span>
</div>
</div>
</div>
<Button size="sm" variant="outline">
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</div>
))}
</div>
{/* Delegation Form */}
{selectedDelegate && (
<Card className="mt-6 border-2 border-green-500">
<CardHeader>
<CardTitle>{t('delegation.delegateTo')} {selectedDelegate.name}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="amount">{t('delegation.amount')}</Label>
<Input
id="amount"
type="number"
placeholder="Enter PZK amount"
value={delegationAmount}
onChange={(e) => setDelegationAmount(e.target.value)}
/>
</div>
<div>
<Label htmlFor="period">{t('delegation.period')}</Label>
<Select value={delegationPeriod} onValueChange={setDelegationPeriod}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1month">1 Month</SelectItem>
<SelectItem value="3months">3 Months</SelectItem>
<SelectItem value="6months">6 Months</SelectItem>
<SelectItem value="1year">1 Year</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>{t('delegation.categories')}</Label>
<div className="flex flex-wrap gap-2 mt-2">
{['Treasury', 'Technical', 'Community', 'Governance'].map((cat) => (
<label key={cat} className="flex items-center gap-2">
<input type="checkbox" className="rounded" />
<span>{cat}</span>
</label>
))}
</div>
</div>
<Button
onClick={handleDelegate}
className="w-full bg-green-600 hover:bg-green-700"
>
{t('delegation.confirmDelegation')}
</Button>
</CardContent>
</Card>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="my-delegations">
<Card className="border-green-200">
<CardHeader>
<CardTitle>{t('delegation.yourDelegations')}</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{myDelegations.map((delegation) => (
<div key={delegation.id} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div>
<h4 className="font-semibold">{delegation.delegate}</h4>
<div className="flex items-center gap-3 text-sm text-gray-600 mt-1">
<span>{delegation.amount} PZK</span>
<Badge variant="secondary">{delegation.category}</Badge>
<span>{delegation.remaining} remaining</span>
</div>
</div>
<Badge className="bg-green-100 text-green-800">
{delegation.status}
</Badge>
</div>
<Progress value={60} className="h-2" />
<div className="flex gap-2 mt-3">
<Button size="sm" variant="outline">
{t('delegation.modify')}
</Button>
<Button size="sm" variant="outline">
{t('delegation.revoke')}
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="delegate-profile">
<DelegateProfile />
</TabsContent>
</Tabs>
</div>
);
};
export default DelegationManager;
+386
View File
@@ -0,0 +1,386 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { ThumbsUp, ThumbsDown, MessageSquare, Shield, Award, TrendingUp, AlertTriangle, MoreVertical, Flag, Edit, Trash2, Loader2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { useWebSocket } from '@/contexts/WebSocketContext';
import { useToast } from '@/hooks/use-toast';
interface Comment {
id: string;
author: string;
avatar: string;
content: string;
timestamp: string;
upvotes: number;
downvotes: number;
isExpert: boolean;
badges: string[];
replies: Comment[];
sentiment: 'positive' | 'neutral' | 'negative';
userVote?: 'up' | 'down' | null;
isLive?: boolean;
}
export function DiscussionThread({ proposalId }: { proposalId: string }) {
const { t } = useTranslation();
const { toast } = useToast();
const { subscribe, unsubscribe, sendMessage, isConnected } = useWebSocket();
const [isLoading, setIsLoading] = useState(false);
const [comments, setComments] = useState<Comment[]>([
{
id: '1',
author: 'Dr. Rojin Ahmed',
avatar: '/api/placeholder/40/40',
content: '## Strong Support for This Proposal\n\nThis proposal addresses a critical need in our governance system. The implementation timeline is realistic and the budget allocation seems appropriate.\n\n**Key Benefits:**\n- Improved transparency\n- Better community engagement\n- Clear accountability metrics\n\nI particularly appreciate the phased approach outlined in section 3.',
timestamp: '2 hours ago',
upvotes: 24,
downvotes: 2,
isExpert: true,
badges: ['Governance Expert', 'Top Contributor'],
sentiment: 'positive',
userVote: null,
replies: [
{
id: '1-1',
author: 'Kawa Mustafa',
avatar: '/api/placeholder/40/40',
content: 'Agreed! The phased approach reduces risk significantly.',
timestamp: '1 hour ago',
upvotes: 8,
downvotes: 0,
isExpert: false,
badges: ['Active Member'],
sentiment: 'positive',
userVote: null,
replies: []
}
]
},
{
id: '2',
author: 'Dilan Karim',
avatar: '/api/placeholder/40/40',
content: '### Concerns About Implementation\n\nWhile I support the overall direction, I have concerns about:\n\n1. The technical complexity might be underestimated\n2. We need more details on the security audit process\n3. Reference to [Proposal #142](/proposals/142) shows similar challenges\n\n> "The devil is in the details" - and we need more of them',
timestamp: '3 hours ago',
upvotes: 18,
downvotes: 5,
isExpert: true,
badges: ['Security Expert'],
sentiment: 'negative',
userVote: null,
replies: []
}
]);
const [newComment, setNewComment] = useState('');
const [replyTo, setReplyTo] = useState<string | null>(null);
const [showMarkdownHelp, setShowMarkdownHelp] = useState(false);
// WebSocket subscriptions for real-time updates
useEffect(() => {
const handleNewComment = (data: any) => {
const newComment: Comment = {
...data,
isLive: true,
};
setComments(prev => [newComment, ...prev]);
// Show notification for mentions
if (data.content.includes('@currentUser')) {
toast({
title: "You were mentioned",
description: `${data.author} mentioned you in a comment`,
});
}
};
const handleVoteUpdate = (data: { commentId: string; upvotes: number; downvotes: number }) => {
setComments(prev => updateVoteCounts(prev, data.commentId, data.upvotes, data.downvotes));
};
const handleSentimentUpdate = (data: { proposalId: string; sentiment: any }) => {
if (data.proposalId === proposalId) {
// Update sentiment visualization in parent component
console.log('Sentiment updated:', data.sentiment);
}
};
subscribe('comment', handleNewComment);
subscribe('vote', handleVoteUpdate);
subscribe('sentiment', handleSentimentUpdate);
return () => {
unsubscribe('comment', handleNewComment);
unsubscribe('vote', handleVoteUpdate);
unsubscribe('sentiment', handleSentimentUpdate);
};
}, [subscribe, unsubscribe, proposalId, toast]);
const updateVoteCounts = (comments: Comment[], targetId: string, upvotes: number, downvotes: number): Comment[] => {
return comments.map(comment => {
if (comment.id === targetId) {
return { ...comment, upvotes, downvotes };
}
if (comment.replies.length > 0) {
return {
...comment,
replies: updateVoteCounts(comment.replies, targetId, upvotes, downvotes)
};
}
return comment;
});
};
const handleVote = useCallback((commentId: string, voteType: 'up' | 'down') => {
const updatedComments = updateCommentVote(comments, commentId, voteType);
setComments(updatedComments);
// Send vote update via WebSocket
const comment = findComment(updatedComments, commentId);
if (comment && isConnected) {
sendMessage({
type: 'vote',
data: {
commentId,
upvotes: comment.upvotes,
downvotes: comment.downvotes,
proposalId,
},
timestamp: Date.now(),
});
}
}, [comments, isConnected, sendMessage, proposalId]);
const findComment = (comments: Comment[], targetId: string): Comment | null => {
for (const comment of comments) {
if (comment.id === targetId) return comment;
const found = findComment(comment.replies, targetId);
if (found) return found;
}
return null;
};
const updateCommentVote = (comments: Comment[], targetId: string, voteType: 'up' | 'down'): Comment[] => {
return comments.map(comment => {
if (comment.id === targetId) {
const wasUpvoted = comment.userVote === 'up';
const wasDownvoted = comment.userVote === 'down';
if (voteType === 'up') {
return {
...comment,
upvotes: wasUpvoted ? comment.upvotes - 1 : comment.upvotes + 1,
downvotes: wasDownvoted ? comment.downvotes - 1 : comment.downvotes,
userVote: wasUpvoted ? null : 'up'
};
} else {
return {
...comment,
upvotes: wasUpvoted ? comment.upvotes - 1 : comment.upvotes,
downvotes: wasDownvoted ? comment.downvotes - 1 : comment.downvotes + 1,
userVote: wasDownvoted ? null : 'down'
};
}
}
if (comment.replies.length > 0) {
return {
...comment,
replies: updateCommentVote(comment.replies, targetId, voteType)
};
}
return comment;
});
};
const renderComment = (comment: Comment, depth: number = 0) => (
<div key={comment.id} className={`${depth > 0 ? 'ml-12 mt-4' : 'mb-6'} ${comment.isLive ? 'animate-pulse-once' : ''}`}>
<Card className="border-l-4 transition-all duration-300" style={{
borderLeftColor: comment.sentiment === 'positive' ? '#10b981' :
comment.sentiment === 'negative' ? '#ef4444' : '#6b7280'
}}>
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3">
<Avatar className="relative">
<AvatarImage src={comment.avatar} />
<AvatarFallback>{comment.author[0]}</AvatarFallback>
{comment.isLive && (
<div className="absolute -top-1 -right-1 h-3 w-3 bg-green-500 rounded-full animate-pulse" />
)}
</Avatar>
<div className="flex-1">
<div className="flex items-center space-x-2">
<span className="font-semibold">{comment.author}</span>
{comment.isExpert && (
<Shield className="h-4 w-4 text-blue-500" />
)}
{comment.badges.map(badge => (
<Badge key={badge} variant="secondary" className="text-xs">
{badge}
</Badge>
))}
<span className="text-sm text-gray-500">
{comment.isLive ? 'Just now' : comment.timestamp}
</span>
{isConnected && (
<div className="h-2 w-2 bg-green-500 rounded-full" title="Real-time updates active" />
)}
</div>
<div className="mt-3 prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: parseMarkdown(comment.content) }} />
<div className="flex items-center space-x-4 mt-4">
<Button
variant={comment.userVote === 'up' ? 'default' : 'ghost'}
size="sm"
onClick={() => handleVote(comment.id, 'up')}
className="transition-all duration-200"
>
<ThumbsUp className="h-4 w-4 mr-1" />
<span className="transition-all duration-300">{comment.upvotes}</span>
</Button>
<Button
variant={comment.userVote === 'down' ? 'default' : 'ghost'}
size="sm"
onClick={() => handleVote(comment.id, 'down')}
className="transition-all duration-200"
>
<ThumbsDown className="h-4 w-4 mr-1" />
<span className="transition-all duration-300">{comment.downvotes}</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setReplyTo(comment.id)}
>
<MessageSquare className="h-4 w-4 mr-1" />
Reply
</Button>
</div>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Flag className="h-4 w-4 mr-2" />
Report
</DropdownMenuItem>
<DropdownMenuItem>
<Edit className="h-4 w-4 mr-2" />
Edit
</DropdownMenuItem>
<DropdownMenuItem className="text-red-600">
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{replyTo === comment.id && (
<div className="mt-4">
<Textarea
placeholder="Write your reply... @mention users to notify them"
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
className="min-h-[80px]"
/>
<div className="flex justify-end space-x-2 mt-2">
<Button variant="outline" onClick={() => setReplyTo(null)}>
Cancel
</Button>
<Button
onClick={() => {
if (newComment.trim() && isConnected) {
sendMessage({
type: 'reply',
data: {
parentId: comment.id,
content: newComment,
proposalId,
author: 'Current User',
},
timestamp: Date.now(),
});
}
setReplyTo(null);
setNewComment('');
}}
disabled={!newComment.trim()}
>
Post Reply
</Button>
</div>
</div>
)}
</CardContent>
</Card>
{comment.replies.map(reply => renderComment(reply, depth + 1))}
</div>
);
const parseMarkdown = (text: string): string => {
return text
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*)\*/gim, '<em>$1</em>')
.replace(/\[([^\]]+)\]\(([^)]+)\)/gim, '<a href="$2" class="text-blue-600 hover:underline">$1</a>')
.replace(/^> (.*$)/gim, '<blockquote class="border-l-4 border-gray-300 pl-4 italic">$1</blockquote>')
.replace(/\n/gim, '<br>');
};
return (
<div className="space-y-6">
<Card>
<CardHeader>
<h3 className="text-xl font-semibold">Discussion Forum</h3>
<p className="text-sm text-gray-600">Share your thoughts and feedback on this proposal</p>
</CardHeader>
<CardContent>
<Textarea
placeholder="Write your comment... (Markdown supported)"
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
className="min-h-[120px]"
/>
<div className="flex justify-between items-center mt-4">
<Button
variant="outline"
size="sm"
onClick={() => setShowMarkdownHelp(!showMarkdownHelp)}
>
Markdown Help
</Button>
<Button>Post Comment</Button>
</div>
{showMarkdownHelp && (
<Card className="mt-4 p-4 bg-gray-50">
<p className="text-sm font-semibold mb-2">Markdown Formatting:</p>
<ul className="text-sm space-y-1">
<li>**bold** <strong>bold</strong></li>
<li>*italic* <em>italic</em></li>
<li>[link](url) <a href="#" className="text-blue-600">link</a></li>
<li>&gt; quote <blockquote className="border-l-4 border-gray-300 pl-2">quote</blockquote></li>
<li># Heading <span className="font-bold text-lg">Heading</span></li>
</ul>
</Card>
)}
</CardContent>
</Card>
<div>
{comments.map(comment => renderComment(comment))}
</div>
</div>
);
}
+250
View File
@@ -0,0 +1,250 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { TrendingUp, TrendingDown, MessageSquare, Users, BarChart3, Search, Filter, Clock, Flame, Award } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { DiscussionThread } from './DiscussionThread';
interface Discussion {
id: string;
title: string;
proposalId: string;
author: string;
category: string;
replies: number;
views: number;
lastActivity: string;
sentiment: number;
trending: boolean;
pinned: boolean;
tags: string[];
}
export function ForumOverview() {
const { t } = useTranslation();
const [selectedDiscussion, setSelectedDiscussion] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState('recent');
const [filterCategory, setFilterCategory] = useState('all');
const discussions: Discussion[] = [
{
id: '1',
title: 'Treasury Allocation for Developer Grants - Q1 2024',
proposalId: 'prop-001',
author: 'Dr. Rojin Ahmed',
category: 'Treasury',
replies: 45,
views: 1234,
lastActivity: '2 hours ago',
sentiment: 72,
trending: true,
pinned: true,
tags: ['treasury', 'grants', 'development']
},
{
id: '2',
title: 'Technical Upgrade: Implementing Zero-Knowledge Proofs',
proposalId: 'prop-002',
author: 'Kawa Mustafa',
category: 'Technical',
replies: 28,
views: 890,
lastActivity: '5 hours ago',
sentiment: 85,
trending: true,
pinned: false,
tags: ['technical', 'zkp', 'privacy']
},
{
id: '3',
title: 'Community Initiative: Education Program for New Users',
proposalId: 'prop-003',
author: 'Dilan Karim',
category: 'Community',
replies: 62,
views: 2100,
lastActivity: '1 day ago',
sentiment: 45,
trending: false,
pinned: false,
tags: ['community', 'education', 'onboarding']
}
];
const sentimentStats = {
positive: 42,
neutral: 35,
negative: 23
};
const getSentimentColor = (sentiment: number) => {
if (sentiment >= 70) return 'text-green-600';
if (sentiment >= 40) return 'text-yellow-600';
return 'text-red-600';
};
const getSentimentIcon = (sentiment: number) => {
if (sentiment >= 70) return <TrendingUp className="h-4 w-4" />;
if (sentiment >= 40) return <BarChart3 className="h-4 w-4" />;
return <TrendingDown className="h-4 w-4" />;
};
if (selectedDiscussion) {
return (
<div className="space-y-6">
<Button
variant="outline"
onClick={() => setSelectedDiscussion(null)}
>
Back to Forum
</Button>
<DiscussionThread proposalId={selectedDiscussion} />
</div>
);
}
return (
<div className="space-y-6">
{/* Sentiment Overview */}
<Card>
<CardHeader>
<CardTitle>Community Sentiment Analysis</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">Positive</span>
<span className="text-sm text-green-600">{sentimentStats.positive}%</span>
</div>
<Progress value={sentimentStats.positive} className="h-2 bg-green-100" />
</div>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">Neutral</span>
<span className="text-sm text-yellow-600">{sentimentStats.neutral}%</span>
</div>
<Progress value={sentimentStats.neutral} className="h-2 bg-yellow-100" />
</div>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">Negative</span>
<span className="text-sm text-red-600">{sentimentStats.negative}%</span>
</div>
<Progress value={sentimentStats.negative} className="h-2 bg-red-100" />
</div>
</div>
</CardContent>
</Card>
{/* Search and Filters */}
<Card>
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="Search discussions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
<Select value={filterCategory} onValueChange={setFilterCategory}>
<SelectTrigger className="w-full md:w-[180px]">
<SelectValue placeholder="Category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
<SelectItem value="treasury">Treasury</SelectItem>
<SelectItem value="technical">Technical</SelectItem>
<SelectItem value="community">Community</SelectItem>
</SelectContent>
</Select>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-full md:w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="recent">Most Recent</SelectItem>
<SelectItem value="popular">Most Popular</SelectItem>
<SelectItem value="replies">Most Replies</SelectItem>
<SelectItem value="sentiment">Best Sentiment</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Discussions List */}
<div className="space-y-4">
{discussions.map((discussion) => (
<Card
key={discussion.id}
className="cursor-pointer hover:shadow-lg transition-shadow"
onClick={() => setSelectedDiscussion(discussion.proposalId)}
>
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
{discussion.pinned && (
<Badge variant="secondary">
📌 Pinned
</Badge>
)}
{discussion.trending && (
<Badge variant="destructive">
<Flame className="h-3 w-3 mr-1" />
Trending
</Badge>
)}
<Badge variant="outline">{discussion.category}</Badge>
</div>
<h3 className="text-lg font-semibold mb-2">{discussion.title}</h3>
<div className="flex items-center gap-4 text-sm text-gray-600">
<span>by {discussion.author}</span>
<span className="flex items-center gap-1">
<MessageSquare className="h-4 w-4" />
{discussion.replies} replies
</span>
<span className="flex items-center gap-1">
<Users className="h-4 w-4" />
{discussion.views} views
</span>
<span className="flex items-center gap-1">
<Clock className="h-4 w-4" />
{discussion.lastActivity}
</span>
</div>
<div className="flex gap-2 mt-3">
{discussion.tags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
#{tag}
</Badge>
))}
</div>
</div>
<div className="text-center ml-6">
<div className={`text-2xl font-bold ${getSentimentColor(discussion.sentiment)}`}>
{discussion.sentiment}%
</div>
<div className="flex items-center gap-1 text-sm text-gray-600">
{getSentimentIcon(discussion.sentiment)}
<span>Sentiment</span>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}
+247
View File
@@ -0,0 +1,247 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { AlertTriangle, Shield, Ban, CheckCircle, Clock, Flag, User, MessageSquare, TrendingUp } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface Report {
id: string;
type: 'spam' | 'harassment' | 'misinformation' | 'other';
reportedContent: string;
reportedBy: string;
reportedUser: string;
timestamp: string;
status: 'pending' | 'reviewing' | 'resolved';
severity: 'low' | 'medium' | 'high';
}
export function ModerationPanel() {
const { t } = useTranslation();
const [autoModeration, setAutoModeration] = useState(true);
const [sentimentThreshold, setSentimentThreshold] = useState(30);
const reports: Report[] = [
{
id: '1',
type: 'misinformation',
reportedContent: 'False claims about proposal implementation...',
reportedBy: 'User123',
reportedUser: 'BadActor456',
timestamp: '10 minutes ago',
status: 'pending',
severity: 'high'
},
{
id: '2',
type: 'spam',
reportedContent: 'Repeated promotional content...',
reportedBy: 'User789',
reportedUser: 'Spammer101',
timestamp: '1 hour ago',
status: 'reviewing',
severity: 'medium'
}
];
const moderationStats = {
totalReports: 24,
resolved: 18,
pending: 6,
bannedUsers: 3,
flaggedContent: 12
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'high': return 'text-red-600 bg-red-100';
case 'medium': return 'text-yellow-600 bg-yellow-100';
case 'low': return 'text-green-600 bg-green-100';
default: return 'text-gray-600 bg-gray-100';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'resolved': return <CheckCircle className="h-4 w-4 text-green-600" />;
case 'reviewing': return <Clock className="h-4 w-4 text-yellow-600" />;
case 'pending': return <AlertTriangle className="h-4 w-4 text-red-600" />;
default: return null;
}
};
return (
<div className="space-y-6">
{/* Moderation Stats */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Total Reports</p>
<p className="text-2xl font-bold">{moderationStats.totalReports}</p>
</div>
<Flag className="h-8 w-8 text-gray-400" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Resolved</p>
<p className="text-2xl font-bold text-green-600">{moderationStats.resolved}</p>
</div>
<CheckCircle className="h-8 w-8 text-green-400" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Pending</p>
<p className="text-2xl font-bold text-yellow-600">{moderationStats.pending}</p>
</div>
<Clock className="h-8 w-8 text-yellow-400" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Banned Users</p>
<p className="text-2xl font-bold text-red-600">{moderationStats.bannedUsers}</p>
</div>
<Ban className="h-8 w-8 text-red-400" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Flagged Content</p>
<p className="text-2xl font-bold">{moderationStats.flaggedContent}</p>
</div>
<AlertTriangle className="h-8 w-8 text-orange-400" />
</div>
</CardContent>
</Card>
</div>
<Tabs defaultValue="reports" className="space-y-4">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="reports">Reports Queue</TabsTrigger>
<TabsTrigger value="settings">Auto-Moderation</TabsTrigger>
<TabsTrigger value="users">User Management</TabsTrigger>
</TabsList>
<TabsContent value="reports" className="space-y-4">
{reports.map((report) => (
<Card key={report.id}>
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
{getStatusIcon(report.status)}
<Badge className={getSeverityColor(report.severity)}>
{report.severity.toUpperCase()}
</Badge>
<Badge variant="outline">{report.type}</Badge>
<span className="text-sm text-gray-500">{report.timestamp}</span>
</div>
<p className="font-medium mb-2">Reported User: {report.reportedUser}</p>
<p className="text-gray-600 mb-3">{report.reportedContent}</p>
<p className="text-sm text-gray-500">Reported by: {report.reportedBy}</p>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm">
Review
</Button>
<Button variant="destructive" size="sm">
Take Action
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</TabsContent>
<TabsContent value="settings" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Auto-Moderation Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label htmlFor="auto-mod">Enable Auto-Moderation</Label>
<p className="text-sm text-gray-600">Automatically flag suspicious content</p>
</div>
<Switch
id="auto-mod"
checked={autoModeration}
onCheckedChange={setAutoModeration}
/>
</div>
<div className="space-y-2">
<Label>Sentiment Threshold</Label>
<p className="text-sm text-gray-600">
Flag comments with sentiment below {sentimentThreshold}%
</p>
<input
type="range"
min="0"
max="100"
value={sentimentThreshold}
onChange={(e) => setSentimentThreshold(Number(e.target.value))}
className="w-full"
/>
</div>
<Alert>
<Shield className="h-4 w-4" />
<AlertDescription>
Auto-moderation uses AI to detect potentially harmful content and automatically flags it for review.
</AlertDescription>
</Alert>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="users" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>User Moderation Actions</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-3">
<User className="h-8 w-8 text-gray-400" />
<div>
<p className="font-medium">BadActor456</p>
<p className="text-sm text-gray-600">3 reports, 2 warnings</p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm">Warn</Button>
<Button variant="outline" size="sm">Suspend</Button>
<Button variant="destructive" size="sm">Ban</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}
@@ -0,0 +1,222 @@
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Users, Vote, Trophy, Clock, AlertCircle, CheckCircle } from 'lucide-react';
interface Election {
id: number;
type: 'Presidential' | 'Parliamentary' | 'Constitutional Court';
status: 'Registration' | 'Campaign' | 'Voting' | 'Completed';
candidates: Candidate[];
totalVotes: number;
endBlock: number;
currentBlock: number;
}
interface Candidate {
id: string;
name: string;
votes: number;
percentage: number;
party?: string;
trustScore: number;
}
const ElectionsInterface: React.FC = () => {
const [selectedElection, setSelectedElection] = useState<Election | null>(null);
const [votedCandidates, setVotedCandidates] = useState<string[]>([]);
const activeElections: Election[] = [
{
id: 1,
type: 'Presidential',
status: 'Voting',
totalVotes: 45678,
endBlock: 1000000,
currentBlock: 995000,
candidates: [
{ id: '1', name: 'Candidate A', votes: 23456, percentage: 51.3, trustScore: 850 },
{ id: '2', name: 'Candidate B', votes: 22222, percentage: 48.7, trustScore: 780 }
]
},
{
id: 2,
type: 'Parliamentary',
status: 'Registration',
totalVotes: 0,
endBlock: 1200000,
currentBlock: 995000,
candidates: []
}
];
const handleVote = (candidateId: string, electionType: string) => {
if (electionType === 'Parliamentary') {
setVotedCandidates(prev =>
prev.includes(candidateId)
? prev.filter(id => id !== candidateId)
: [...prev, candidateId]
);
} else {
setVotedCandidates([candidateId]);
}
};
return (
<div className="space-y-6">
<Tabs defaultValue="active" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="active">Active Elections</TabsTrigger>
<TabsTrigger value="register">Register</TabsTrigger>
<TabsTrigger value="results">Results</TabsTrigger>
</TabsList>
<TabsContent value="active" className="space-y-4">
{activeElections.map(election => (
<Card key={election.id}>
<CardHeader>
<div className="flex justify-between items-start">
<div>
<CardTitle>{election.type} Election</CardTitle>
<CardDescription>
{election.status === 'Voting'
? `${election.totalVotes.toLocaleString()} votes cast`
: `Registration ends in ${(election.endBlock - election.currentBlock).toLocaleString()} blocks`}
</CardDescription>
</div>
<Badge variant={election.status === 'Voting' ? 'default' : 'secondary'}>
{election.status}
</Badge>
</div>
</CardHeader>
<CardContent>
{election.status === 'Voting' && (
<div className="space-y-4">
{election.candidates.map(candidate => (
<div key={candidate.id} className="space-y-2">
<div className="flex justify-between items-center">
<div>
<p className="font-medium">{candidate.name}</p>
<p className="text-sm text-muted-foreground">
Trust Score: {candidate.trustScore}
</p>
</div>
<div className="text-right">
<p className="font-bold">{candidate.percentage}%</p>
<p className="text-sm text-muted-foreground">
{candidate.votes.toLocaleString()} votes
</p>
</div>
</div>
<Progress value={candidate.percentage} className="h-2" />
<Button
size="sm"
variant={votedCandidates.includes(candidate.id) ? "default" : "outline"}
onClick={() => handleVote(candidate.id, election.type)}
className="w-full"
>
{votedCandidates.includes(candidate.id) ? (
<>
<CheckCircle className="w-4 h-4 mr-2" />
Voted
</>
) : (
<>
<Vote className="w-4 h-4 mr-2" />
Vote
</>
)}
</Button>
</div>
))}
{election.type === 'Parliamentary' && (
<p className="text-sm text-muted-foreground text-center">
You can select multiple candidates
</p>
)}
</div>
)}
</CardContent>
</Card>
))}
</TabsContent>
<TabsContent value="register">
<Card>
<CardHeader>
<CardTitle>Candidate Registration</CardTitle>
<CardDescription>
Register as a candidate for upcoming elections
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg">
<div className="flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
<div>
<p className="font-medium text-amber-900 dark:text-amber-100">
Requirements
</p>
<ul className="text-sm text-amber-800 dark:text-amber-200 mt-2 space-y-1">
<li> Minimum Trust Score: 300 (Parliamentary) / 600 (Presidential)</li>
<li> KYC Approved Status</li>
<li> Endorsements: 10 (Parliamentary) / 50 (Presidential)</li>
<li> Deposit: 1000 PEZ</li>
</ul>
</div>
</div>
</div>
<Button className="w-full" size="lg">
Register as Candidate
</Button>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="results">
<Card>
<CardHeader>
<CardTitle>Election Results</CardTitle>
<CardDescription>Historical election outcomes</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="p-4 border rounded-lg">
<div className="flex justify-between items-start mb-3">
<div>
<p className="font-medium">Presidential Election 2024</p>
<p className="text-sm text-muted-foreground">Completed 30 days ago</p>
</div>
<Badge variant="outline">
<Trophy className="w-3 h-3 mr-1" />
Completed
</Badge>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>Winner: Candidate A</span>
<span className="font-bold">52.8%</span>
</div>
<div className="flex justify-between text-sm text-muted-foreground">
<span>Total Votes</span>
<span>89,234</span>
</div>
<div className="flex justify-between text-sm text-muted-foreground">
<span>Turnout</span>
<span>67.5%</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
};
export default ElectionsInterface;
@@ -0,0 +1,243 @@
import React, { useState } from 'react';
import {
Vote, Users, Gavel, FileText, TrendingUpIcon,
Clock, CheckCircle, XCircle, AlertCircle,
BarChart3, PieChart, Activity, Shield
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Progress } from '../ui/progress';
interface GovernanceStats {
activeProposals: number;
activeElections: number;
totalVoters: number;
participationRate: number;
parliamentMembers: number;
diwanMembers: number;
nextElection: string;
treasuryBalance: string;
}
const GovernanceOverview: React.FC = () => {
const [stats] = useState<GovernanceStats>({
activeProposals: 12,
activeElections: 2,
totalVoters: 15234,
participationRate: 68.5,
parliamentMembers: 27,
diwanMembers: 9,
nextElection: '15 days',
treasuryBalance: '2.5M PEZ'
});
const [recentActivity] = useState([
{ type: 'proposal', action: 'New proposal submitted', title: 'Treasury Allocation Update', time: '2 hours ago' },
{ type: 'vote', action: 'Vote cast', title: 'Infrastructure Development Fund', time: '3 hours ago' },
{ type: 'election', action: 'Election started', title: 'Parliamentary Elections 2024', time: '1 day ago' },
{ type: 'approved', action: 'Proposal approved', title: 'Community Grant Program', time: '2 days ago' }
]);
const getActivityIcon = (type: string) => {
switch(type) {
case 'proposal': return <FileText className="w-4 h-4 text-blue-400" />;
case 'vote': return <Vote className="w-4 h-4 text-purple-400" />;
case 'election': return <Users className="w-4 h-4 text-cyan-400" />;
case 'approved': return <CheckCircle className="w-4 h-4 text-green-400" />;
default: return <Activity className="w-4 h-4 text-gray-400" />;
}
};
return (
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm">Active Proposals</p>
<p className="text-2xl font-bold text-white mt-1">{stats.activeProposals}</p>
<p className="text-xs text-green-400 mt-2">+3 this week</p>
</div>
<div className="p-3 bg-blue-500/10 rounded-lg">
<FileText className="w-6 h-6 text-blue-400" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm">Active Elections</p>
<p className="text-2xl font-bold text-white mt-1">{stats.activeElections}</p>
<p className="text-xs text-cyan-400 mt-2">Next in {stats.nextElection}</p>
</div>
<div className="p-3 bg-cyan-500/10 rounded-lg">
<Users className="w-6 h-6 text-cyan-400" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm">Participation Rate</p>
<p className="text-2xl font-bold text-white mt-1">{stats.participationRate}%</p>
<Progress value={stats.participationRate} className="mt-2 h-1" />
</div>
<div className="p-3 bg-kurdish-green/10 rounded-lg">
<TrendingUpIcon className="w-6 h-6 text-kurdish-green" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-400 text-sm">Treasury Balance</p>
<p className="text-2xl font-bold text-white mt-1">{stats.treasuryBalance}</p>
<p className="text-xs text-yellow-400 mt-2">Available for proposals</p>
</div>
<div className="p-3 bg-yellow-500/10 rounded-lg">
<Shield className="w-6 h-6 text-yellow-400" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Government Bodies */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card className="bg-gray-900/50 border-gray-800">
<CardHeader>
<CardTitle className="text-white flex items-center">
<Gavel className="w-5 h-5 mr-2 text-purple-400" />
Parliament Status
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-gray-400">Active Members</span>
<span className="text-white font-semibold">{stats.parliamentMembers}/27</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-400">Current Session</span>
<Badge className="bg-green-500/10 text-green-400 border-green-500/20">In Session</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-400">Pending Votes</span>
<span className="text-white font-semibold">5</span>
</div>
<div className="pt-2 border-t border-gray-800">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Quorum Status</span>
<span className="text-green-400">Met (85%)</span>
</div>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardHeader>
<CardTitle className="text-white flex items-center">
<Shield className="w-5 h-5 mr-2 text-cyan-400" />
Dîwan (Constitutional Court)
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-gray-400">Active Judges</span>
<span className="text-white font-semibold">{stats.diwanMembers}/9</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-400">Pending Reviews</span>
<span className="text-white font-semibold">3</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-400">Recent Decisions</span>
<span className="text-white font-semibold">12</span>
</div>
<div className="pt-2 border-t border-gray-800">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Next Hearing</span>
<span className="text-cyan-400">Tomorrow, 14:00 UTC</span>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Recent Activity */}
<Card className="bg-gray-900/50 border-gray-800">
<CardHeader>
<CardTitle className="text-white flex items-center">
<Activity className="w-5 h-5 mr-2 text-purple-400" />
Recent Governance Activity
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{recentActivity.map((activity, index) => (
<div key={index} className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-800/50 transition-colors">
{getActivityIcon(activity.type)}
<div className="flex-1">
<p className="text-sm text-gray-300">{activity.action}</p>
<p className="text-xs text-white font-medium mt-1">{activity.title}</p>
</div>
<span className="text-xs text-gray-500">{activity.time}</span>
</div>
))}
</div>
</CardContent>
</Card>
{/* Voting Power Distribution */}
<Card className="bg-gray-900/50 border-gray-800">
<CardHeader>
<CardTitle className="text-white flex items-center">
<PieChart className="w-5 h-5 mr-2 text-purple-400" />
Voting Power Distribution
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-gray-400">Direct Votes</span>
<span className="text-white font-semibold">45%</span>
</div>
<Progress value={45} className="h-2 bg-gray-800" />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-gray-400">Delegated Votes</span>
<span className="text-white font-semibold">35%</span>
</div>
<Progress value={35} className="h-2 bg-gray-800" />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-gray-400">Proxy Votes</span>
<span className="text-white font-semibold">20%</span>
</div>
<Progress value={20} className="h-2 bg-gray-800" />
</div>
</div>
</CardContent>
</Card>
</div>
);
};
export default GovernanceOverview;
+156
View File
@@ -0,0 +1,156 @@
import React, { useState } from 'react';
import { FileText, Vote, Clock, TrendingUp, Users, AlertCircle } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
import { Progress } from '../ui/progress';
interface Proposal {
id: number;
title: string;
description: string;
proposer: string;
type: 'treasury' | 'executive' | 'constitutional' | 'simple';
status: 'active' | 'passed' | 'rejected' | 'pending';
ayeVotes: number;
nayVotes: number;
totalVotes: number;
quorum: number;
deadline: string;
requestedAmount?: string;
}
const ProposalsList: React.FC = () => {
const [proposals] = useState<Proposal[]>([
{
id: 1,
title: 'Treasury Allocation for Development Fund',
description: 'Allocate 500,000 PEZ for ecosystem development',
proposer: '5GrwvaEF...',
type: 'treasury',
status: 'active',
ayeVotes: 156,
nayVotes: 45,
totalVotes: 201,
quorum: 60,
deadline: '2 days',
requestedAmount: '500,000 PEZ'
},
{
id: 2,
title: 'Update Staking Parameters',
description: 'Increase minimum stake requirement to 1000 HEZ',
proposer: '5FHneW46...',
type: 'executive',
status: 'active',
ayeVotes: 89,
nayVotes: 112,
totalVotes: 201,
quorum: 60,
deadline: '5 days'
}
]);
const getStatusBadge = (status: string) => {
switch(status) {
case 'active': return <Badge className="bg-blue-500/10 text-blue-400">Active</Badge>;
case 'passed': return <Badge className="bg-green-500/10 text-green-400">Passed</Badge>;
case 'rejected': return <Badge className="bg-red-500/10 text-red-400">Rejected</Badge>;
default: return <Badge className="bg-gray-500/10 text-gray-400">Pending</Badge>;
}
};
const getTypeBadge = (type: string) => {
switch(type) {
case 'treasury': return <Badge className="bg-yellow-500/10 text-yellow-400">Treasury</Badge>;
case 'executive': return <Badge className="bg-kurdish-red/10 text-kurdish-red">Executive</Badge>;
case 'constitutional': return <Badge className="bg-cyan-500/10 text-cyan-400">Constitutional</Badge>;
default: return <Badge className="bg-gray-500/10 text-gray-400">Simple</Badge>;
}
};
return (
<div className="space-y-4">
{proposals.map((proposal) => {
const ayePercentage = (proposal.ayeVotes / proposal.totalVotes) * 100;
const nayPercentage = (proposal.nayVotes / proposal.totalVotes) * 100;
const quorumReached = (proposal.totalVotes / 300) * 100 >= proposal.quorum;
return (
<Card key={proposal.id} className="bg-gray-900/50 border-gray-800">
<CardHeader>
<div className="flex items-start justify-between">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<span className="text-gray-400 text-sm">#{proposal.id}</span>
{getTypeBadge(proposal.type)}
{getStatusBadge(proposal.status)}
</div>
<CardTitle className="text-white text-lg">{proposal.title}</CardTitle>
<p className="text-gray-400 text-sm">{proposal.description}</p>
</div>
<div className="text-right">
<div className="flex items-center text-gray-400 text-sm">
<Clock className="w-4 h-4 mr-1" />
{proposal.deadline}
</div>
{proposal.requestedAmount && (
<div className="mt-2 text-yellow-400 font-semibold">
{proposal.requestedAmount}
</div>
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Voting Progress</span>
<span className="text-white">{proposal.totalVotes} votes</span>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<span className="text-green-400 text-xs w-12">Aye</span>
<Progress value={ayePercentage} className="flex-1 h-2" />
<span className="text-white text-sm w-12 text-right">{ayePercentage.toFixed(0)}%</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-red-400 text-xs w-12">Nay</span>
<Progress value={nayPercentage} className="flex-1 h-2" />
<span className="text-white text-sm w-12 text-right">{nayPercentage.toFixed(0)}%</span>
</div>
</div>
</div>
<div className="flex items-center justify-between pt-3 border-t border-gray-800">
<div className="flex items-center space-x-4 text-sm">
<div className="flex items-center">
<Users className="w-4 h-4 mr-1 text-gray-400" />
<span className="text-gray-400">Proposer: {proposal.proposer}</span>
</div>
<div className="flex items-center">
{quorumReached ? (
<span className="text-green-400"> Quorum reached</span>
) : (
<span className="text-yellow-400"> Quorum: {proposal.quorum}%</span>
)}
</div>
</div>
<div className="flex items-center space-x-2">
<Button size="sm" variant="outline" className="border-gray-700">
View Details
</Button>
<Button size="sm" className="bg-kurdish-green hover:bg-kurdish-green/80">
Cast Vote
</Button>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
);
};
export default ProposalsList;
@@ -0,0 +1,206 @@
import { useState, useEffect } from 'react';
import { Bell, Check, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useAuth } from '@/contexts/AuthContext';
import { supabase } from '@/lib/supabase';
import { formatDistanceToNow } from 'date-fns';
interface Notification {
id: string;
title: string;
message: string;
type: 'info' | 'success' | 'warning' | 'error' | 'system';
read: boolean;
action_url?: string;
created_at: string;
}
export default function NotificationBell() {
const { user } = useAuth();
const [notifications, setNotifications] = useState<Notification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [open, setOpen] = useState(false);
useEffect(() => {
if (user) {
loadNotifications();
subscribeToNotifications();
}
}, [user]);
const loadNotifications = async () => {
if (!user) return;
const { data } = await supabase
.from('notifications')
.select('*')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
.limit(10);
if (data) {
setNotifications(data);
setUnreadCount(data.filter(n => !n.read).length);
}
};
const subscribeToNotifications = () => {
const channel = supabase
.channel('notifications')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'notifications',
filter: `user_id=eq.${user?.id}`,
},
(payload) => {
setNotifications(prev => [payload.new as Notification, ...prev]);
setUnreadCount(prev => prev + 1);
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
};
const markAsRead = async (notificationId: string) => {
await supabase.functions.invoke('notifications-manager', {
body: {
action: 'markRead',
userId: user?.id,
notificationId
}
});
setNotifications(prev =>
prev.map(n => n.id === notificationId ? { ...n, read: true } : n)
);
setUnreadCount(prev => Math.max(0, prev - 1));
};
const markAllAsRead = async () => {
await supabase.functions.invoke('notifications-manager', {
body: {
action: 'markAllRead',
userId: user?.id
}
});
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
setUnreadCount(0);
};
const deleteNotification = async (notificationId: string) => {
await supabase.functions.invoke('notifications-manager', {
body: {
action: 'delete',
userId: user?.id,
notificationId
}
});
setNotifications(prev => prev.filter(n => n.id !== notificationId));
};
const getTypeColor = (type: string) => {
switch (type) {
case 'success': return 'text-green-600';
case 'warning': return 'text-yellow-600';
case 'error': return 'text-red-600';
case 'system': return 'text-blue-600';
default: return 'text-gray-600';
}
};
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<Bell className="h-5 w-5" />
{unreadCount > 0 && (
<Badge className="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center">
{unreadCount}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<div className="flex items-center justify-between p-4 border-b">
<h3 className="font-semibold">Notifications</h3>
{unreadCount > 0 && (
<Button
variant="ghost"
size="sm"
onClick={markAllAsRead}
>
Mark all read
</Button>
)}
</div>
<ScrollArea className="h-96">
{notifications.length === 0 ? (
<div className="p-4 text-center text-muted-foreground">
No notifications
</div>
) : (
<div className="divide-y">
{notifications.map((notification) => (
<div
key={notification.id}
className={`p-4 hover:bg-muted/50 transition-colors ${
!notification.read ? 'bg-muted/20' : ''
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className={`font-medium ${getTypeColor(notification.type)}`}>
{notification.title}
</p>
<p className="text-sm text-muted-foreground mt-1">
{notification.message}
</p>
<p className="text-xs text-muted-foreground mt-2">
{formatDistanceToNow(new Date(notification.created_at), { addSuffix: true })}
</p>
</div>
<div className="flex items-center gap-1 ml-2">
{!notification.read && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => markAsRead(notification.id)}
>
<Check className="h-4 w-4" />
</Button>
)}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => deleteNotification(notification.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
))}
</div>
)}
</ScrollArea>
</PopoverContent>
</Popover>
);
}
@@ -0,0 +1,265 @@
import React, { useState, useEffect } from 'react';
import { Bell, MessageCircle, AtSign, Heart, Award, TrendingUp, X, Check, Settings } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { useWebSocket } from '@/contexts/WebSocketContext';
import { useToast } from '@/hooks/use-toast';
import { useTranslation } from 'react-i18next';
interface Notification {
id: string;
type: 'mention' | 'reply' | 'vote' | 'badge' | 'proposal';
title: string;
message: string;
timestamp: Date;
read: boolean;
actionUrl?: string;
sender?: {
name: string;
avatar: string;
};
}
export const NotificationCenter: React.FC = () => {
const { t } = useTranslation();
const { subscribe, unsubscribe } = useWebSocket();
const { toast } = useToast();
const [notifications, setNotifications] = useState<Notification[]>([]);
const [isOpen, setIsOpen] = useState(false);
const [unreadCount, setUnreadCount] = useState(0);
const [settings, setSettings] = useState({
mentions: true,
replies: true,
votes: true,
badges: true,
proposals: true,
pushEnabled: false,
});
useEffect(() => {
// Request notification permission
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
// Subscribe to WebSocket events
const handleMention = (data: any) => {
const notification: Notification = {
id: Date.now().toString(),
type: 'mention',
title: 'You were mentioned',
message: `${data.sender} mentioned you in a discussion`,
timestamp: new Date(),
read: false,
actionUrl: data.url,
sender: data.senderInfo,
};
addNotification(notification);
};
const handleReply = (data: any) => {
const notification: Notification = {
id: Date.now().toString(),
type: 'reply',
title: 'New reply',
message: `${data.sender} replied to your comment`,
timestamp: new Date(),
read: false,
actionUrl: data.url,
sender: data.senderInfo,
};
addNotification(notification);
};
subscribe('mention', handleMention);
subscribe('reply', handleReply);
return () => {
unsubscribe('mention', handleMention);
unsubscribe('reply', handleReply);
};
}, [subscribe, unsubscribe]);
const addNotification = (notification: Notification) => {
setNotifications(prev => [notification, ...prev]);
setUnreadCount(prev => prev + 1);
// Show toast
toast({
title: notification.title,
description: notification.message,
});
// Show push notification if enabled
if (settings.pushEnabled && 'Notification' in window && Notification.permission === 'granted') {
new Notification(notification.title, {
body: notification.message,
icon: '/logo.png',
});
}
};
const markAsRead = (id: string) => {
setNotifications(prev =>
prev.map(n => n.id === id ? { ...n, read: true } : n)
);
setUnreadCount(prev => Math.max(0, prev - 1));
};
const markAllAsRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
setUnreadCount(0);
};
const getIcon = (type: string) => {
switch (type) {
case 'mention': return <AtSign className="h-4 w-4" />;
case 'reply': return <MessageCircle className="h-4 w-4" />;
case 'vote': return <Heart className="h-4 w-4" />;
case 'badge': return <Award className="h-4 w-4" />;
case 'proposal': return <TrendingUp className="h-4 w-4" />;
default: return <Bell className="h-4 w-4" />;
}
};
return (
<div className="relative">
<Button
variant="ghost"
size="icon"
onClick={() => setIsOpen(!isOpen)}
className="relative"
>
<Bell className="h-5 w-5" />
{unreadCount > 0 && (
<Badge className="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center">
{unreadCount}
</Badge>
)}
</Button>
{isOpen && (
<Card className="absolute right-0 top-12 w-96 z-50">
<Tabs defaultValue="all" className="w-full">
<div className="flex items-center justify-between p-4 border-b">
<TabsList>
<TabsTrigger value="all">All</TabsTrigger>
<TabsTrigger value="unread">Unread</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<Button variant="ghost" size="icon" onClick={() => setIsOpen(false)}>
<X className="h-4 w-4" />
</Button>
</div>
<TabsContent value="all" className="p-0">
<div className="flex items-center justify-between px-4 py-2 border-b">
<span className="text-sm text-muted-foreground">
{notifications.length} notifications
</span>
<Button variant="ghost" size="sm" onClick={markAllAsRead}>
<Check className="h-3 w-3 mr-1" />
Mark all read
</Button>
</div>
<ScrollArea className="h-96">
{notifications.map(notification => (
<div
key={notification.id}
className={`p-4 border-b hover:bg-accent cursor-pointer ${
!notification.read ? 'bg-accent/50' : ''
}`}
onClick={() => markAsRead(notification.id)}
>
<div className="flex items-start gap-3">
<div className="p-2 rounded-full bg-primary/10">
{getIcon(notification.type)}
</div>
<div className="flex-1">
<p className="font-medium text-sm">{notification.title}</p>
<p className="text-sm text-muted-foreground mt-1">
{notification.message}
</p>
<p className="text-xs text-muted-foreground mt-2">
{new Date(notification.timestamp).toLocaleTimeString()}
</p>
</div>
</div>
</div>
))}
</ScrollArea>
</TabsContent>
<TabsContent value="unread" className="p-0">
<ScrollArea className="h-96">
{notifications.filter(n => !n.read).map(notification => (
<div
key={notification.id}
className="p-4 border-b hover:bg-accent cursor-pointer bg-accent/50"
onClick={() => markAsRead(notification.id)}
>
<div className="flex items-start gap-3">
<div className="p-2 rounded-full bg-primary/10">
{getIcon(notification.type)}
</div>
<div className="flex-1">
<p className="font-medium text-sm">{notification.title}</p>
<p className="text-sm text-muted-foreground mt-1">
{notification.message}
</p>
</div>
</div>
</div>
))}
</ScrollArea>
</TabsContent>
<TabsContent value="settings" className="p-4">
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor="push">Push Notifications</Label>
<Switch
id="push"
checked={settings.pushEnabled}
onCheckedChange={(checked) => {
if (checked && 'Notification' in window) {
Notification.requestPermission().then(permission => {
setSettings(prev => ({ ...prev, pushEnabled: permission === 'granted' }));
});
} else {
setSettings(prev => ({ ...prev, pushEnabled: checked }));
}
}}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="mentions">Mentions</Label>
<Switch
id="mentions"
checked={settings.mentions}
onCheckedChange={(checked) =>
setSettings(prev => ({ ...prev, mentions: checked }))}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="replies">Replies</Label>
<Switch
id="replies"
checked={settings.replies}
onCheckedChange={(checked) =>
setSettings(prev => ({ ...prev, replies: checked }))}
/>
</div>
</div>
</TabsContent>
</Tabs>
</Card>
)}
</div>
);
};
+330
View File
@@ -0,0 +1,330 @@
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ArrowUpDown, Search, Filter, TrendingUp, TrendingDown, User, Shield, Clock, DollarSign } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface P2POffer {
id: string;
type: 'buy' | 'sell';
token: 'HEZ' | 'PEZ';
amount: number;
price: number;
paymentMethod: string;
seller: {
name: string;
rating: number;
completedTrades: number;
verified: boolean;
};
minOrder: number;
maxOrder: number;
timeLimit: number;
}
export const P2PMarket: React.FC = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<'buy' | 'sell'>('buy');
const [selectedToken, setSelectedToken] = useState<'HEZ' | 'PEZ'>('HEZ');
const [searchTerm, setSearchTerm] = useState('');
const [selectedOffer, setSelectedOffer] = useState<P2POffer | null>(null);
const [tradeAmount, setTradeAmount] = useState('');
const offers: P2POffer[] = [
{
id: '1',
type: 'sell',
token: 'HEZ',
amount: 10000,
price: 0.95,
paymentMethod: 'Bank Transfer',
seller: {
name: 'CryptoTrader',
rating: 4.8,
completedTrades: 234,
verified: true
},
minOrder: 100,
maxOrder: 5000,
timeLimit: 30
},
{
id: '2',
type: 'sell',
token: 'HEZ',
amount: 5000,
price: 0.96,
paymentMethod: 'PayPal',
seller: {
name: 'TokenMaster',
rating: 4.9,
completedTrades: 567,
verified: true
},
minOrder: 50,
maxOrder: 2000,
timeLimit: 15
},
{
id: '3',
type: 'buy',
token: 'PEZ',
amount: 15000,
price: 1.02,
paymentMethod: 'Crypto',
seller: {
name: 'PezWhale',
rating: 4.7,
completedTrades: 123,
verified: false
},
minOrder: 500,
maxOrder: 10000,
timeLimit: 60
},
{
id: '4',
type: 'sell',
token: 'PEZ',
amount: 8000,
price: 1.01,
paymentMethod: 'Wire Transfer',
seller: {
name: 'QuickTrade',
rating: 4.6,
completedTrades: 89,
verified: true
},
minOrder: 200,
maxOrder: 3000,
timeLimit: 45
}
];
const filteredOffers = offers.filter(offer =>
offer.type === activeTab &&
offer.token === selectedToken &&
(searchTerm === '' || offer.seller.name.toLowerCase().includes(searchTerm.toLowerCase()))
);
const handleTrade = (offer: P2POffer) => {
console.log('Initiating trade:', tradeAmount, offer.token, 'with', offer.seller.name);
// Implement trade logic
};
return (
<div className="space-y-6">
{/* Market Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">HEZ Price</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">$0.95</div>
<div className="flex items-center text-green-500 text-xs mt-1">
<TrendingUp className="w-3 h-3 mr-1" />
+2.3%
</div>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">PEZ Price</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">$1.02</div>
<div className="flex items-center text-red-500 text-xs mt-1">
<TrendingDown className="w-3 h-3 mr-1" />
-0.8%
</div>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">24h Volume</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">$2.4M</div>
<p className="text-xs text-gray-500 mt-1">1,234 trades</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">Active Offers</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">342</div>
<p className="text-xs text-gray-500 mt-1">89 verified sellers</p>
</CardContent>
</Card>
</div>
{/* P2P Trading Interface */}
<Card className="bg-gray-900 border-gray-800">
<CardHeader>
<CardTitle className="text-xl text-white">P2P Market</CardTitle>
<CardDescription className="text-gray-400">
Buy and sell tokens directly with other users
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Filters */}
<div className="flex flex-wrap gap-4">
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as 'buy' | 'sell')} className="flex-1">
<TabsList className="grid w-full max-w-[200px] grid-cols-2">
<TabsTrigger value="buy">Buy</TabsTrigger>
<TabsTrigger value="sell">Sell</TabsTrigger>
</TabsList>
</Tabs>
<Select value={selectedToken} onValueChange={(v) => setSelectedToken(v as 'HEZ' | 'PEZ')}>
<SelectTrigger className="w-[120px] bg-gray-800 border-gray-700">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="HEZ">HEZ</SelectItem>
<SelectItem value="PEZ">PEZ</SelectItem>
</SelectContent>
</Select>
<div className="flex-1 max-w-xs">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="Search sellers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 bg-gray-800 border-gray-700"
/>
</div>
</div>
</div>
{/* Offers List */}
<div className="space-y-3">
{filteredOffers.map((offer) => (
<Card key={offer.id} className="bg-gray-800 border-gray-700">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="w-10 h-10 bg-gray-700 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-gray-400" />
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-semibold text-white">{offer.seller.name}</span>
{offer.seller.verified && (
<Badge variant="secondary" className="bg-blue-600/20 text-blue-400">
<Shield className="w-3 h-3 mr-1" />
Verified
</Badge>
)}
</div>
<div className="flex items-center gap-3 text-sm text-gray-400">
<span> {offer.seller.rating}</span>
<span>{offer.seller.completedTrades} trades</span>
<span>{offer.paymentMethod}</span>
</div>
</div>
</div>
<div className="text-right space-y-1">
<div className="text-lg font-bold text-white">
${offer.price} / {offer.token}
</div>
<div className="text-sm text-gray-400">
Available: {offer.amount.toLocaleString()} {offer.token}
</div>
<div className="text-xs text-gray-500">
Limits: {offer.minOrder} - {offer.maxOrder} {offer.token}
</div>
</div>
<Button
className="ml-4 bg-green-600 hover:bg-green-700"
onClick={() => setSelectedOffer(offer)}
>
{activeTab === 'buy' ? 'Buy' : 'Sell'} {offer.token}
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</CardContent>
</Card>
{/* Trade Modal */}
{selectedOffer && (
<Card className="bg-gray-900 border-gray-800">
<CardHeader>
<CardTitle>
{activeTab === 'buy' ? 'Buy' : 'Sell'} {selectedOffer.token} from {selectedOffer.seller.name}
</CardTitle>
<CardDescription>Complete your P2P trade</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label>Amount ({selectedOffer.token})</Label>
<Input
type="number"
placeholder={`Min: ${selectedOffer.minOrder}, Max: ${selectedOffer.maxOrder}`}
value={tradeAmount}
onChange={(e) => setTradeAmount(e.target.value)}
className="bg-gray-800 border-gray-700"
/>
</div>
<div className="bg-gray-800 p-4 rounded-lg space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Price per {selectedOffer.token}</span>
<span className="text-white">${selectedOffer.price}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Total Amount</span>
<span className="text-white font-semibold">
${(parseFloat(tradeAmount || '0') * selectedOffer.price).toFixed(2)}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Payment Method</span>
<span className="text-white">{selectedOffer.paymentMethod}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Time Limit</span>
<span className="text-white">{selectedOffer.timeLimit} minutes</span>
</div>
</div>
<div className="flex gap-3">
<Button
className="flex-1 bg-green-600 hover:bg-green-700"
onClick={() => handleTrade(selectedOffer)}
>
Confirm {activeTab === 'buy' ? 'Purchase' : 'Sale'}
</Button>
<Button
variant="outline"
className="flex-1"
onClick={() => setSelectedOffer(null)}
>
Cancel
</Button>
</div>
</CardContent>
</Card>
)}
</div>
);
};
+352
View File
@@ -0,0 +1,352 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Progress } from '@/components/ui/progress';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { FileText, DollarSign, Code, Users, ChevronRight, ChevronLeft, Check } from 'lucide-react';
interface ProposalWizardProps {
onComplete: (proposal: any) => void;
onCancel: () => void;
}
const ProposalWizard: React.FC<ProposalWizardProps> = ({ onComplete, onCancel }) => {
const { t } = useTranslation();
const [currentStep, setCurrentStep] = useState(1);
const [selectedTemplate, setSelectedTemplate] = useState('');
const [proposalData, setProposalData] = useState({
title: '',
category: '',
summary: '',
description: '',
motivation: '',
specification: '',
budget: '',
timeline: '',
milestones: [''],
risks: '',
team: '',
impact: '',
metrics: ''
});
const templates = [
{
id: 'treasury',
name: t('proposals.templates.treasury'),
icon: DollarSign,
description: t('proposals.templates.treasuryDesc'),
color: 'bg-green-500'
},
{
id: 'technical',
name: t('proposals.templates.technical'),
icon: Code,
description: t('proposals.templates.technicalDesc'),
color: 'bg-blue-500'
},
{
id: 'community',
name: t('proposals.templates.community'),
icon: Users,
description: t('proposals.templates.communityDesc'),
color: 'bg-purple-500'
}
];
const steps = [
{ id: 1, name: t('proposals.steps.template') },
{ id: 2, name: t('proposals.steps.basics') },
{ id: 3, name: t('proposals.steps.details') },
{ id: 4, name: t('proposals.steps.impact') },
{ id: 5, name: t('proposals.steps.review') }
];
const handleNext = () => {
if (currentStep < steps.length) {
setCurrentStep(currentStep + 1);
}
};
const handleBack = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const handleSubmit = () => {
onComplete({ ...proposalData, template: selectedTemplate });
};
const progress = (currentStep / steps.length) * 100;
return (
<div className="max-w-4xl mx-auto p-6">
{/* Progress Bar */}
<div className="mb-8">
<div className="flex justify-between mb-2">
{steps.map((step) => (
<div
key={step.id}
className={`text-sm font-medium ${
step.id <= currentStep ? 'text-green-600' : 'text-gray-400'
}`}
>
{step.name}
</div>
))}
</div>
<Progress value={progress} className="h-2" />
</div>
{/* Step Content */}
<Card className="border-green-200">
<CardHeader>
<CardTitle>{steps[currentStep - 1].name}</CardTitle>
<CardDescription>
{currentStep === 1 && t('proposals.wizard.selectTemplate')}
{currentStep === 2 && t('proposals.wizard.enterBasics')}
{currentStep === 3 && t('proposals.wizard.provideDetails')}
{currentStep === 4 && t('proposals.wizard.defineImpact')}
{currentStep === 5 && t('proposals.wizard.reviewSubmit')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Step 1: Template Selection */}
{currentStep === 1 && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{templates.map((template) => {
const Icon = template.icon;
return (
<Card
key={template.id}
className={`cursor-pointer transition-all ${
selectedTemplate === template.id
? 'border-green-500 shadow-lg'
: 'hover:border-gray-300'
}`}
onClick={() => setSelectedTemplate(template.id)}
>
<CardContent className="p-6 text-center">
<div className={`w-16 h-16 mx-auto mb-4 rounded-full ${template.color} flex items-center justify-center`}>
<Icon className="w-8 h-8 text-white" />
</div>
<h3 className="font-semibold mb-2">{template.name}</h3>
<p className="text-sm text-gray-600">{template.description}</p>
</CardContent>
</Card>
);
})}
</div>
)}
{/* Step 2: Basic Information */}
{currentStep === 2 && (
<div className="space-y-4">
<div>
<Label htmlFor="title">{t('proposals.fields.title')}</Label>
<Input
id="title"
value={proposalData.title}
onChange={(e) => setProposalData({...proposalData, title: e.target.value})}
placeholder={t('proposals.placeholders.title')}
/>
</div>
<div>
<Label htmlFor="category">{t('proposals.fields.category')}</Label>
<Select
value={proposalData.category}
onValueChange={(value) => setProposalData({...proposalData, category: value})}
>
<SelectTrigger>
<SelectValue placeholder={t('proposals.placeholders.category')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="treasury">Treasury</SelectItem>
<SelectItem value="technical">Technical</SelectItem>
<SelectItem value="community">Community</SelectItem>
<SelectItem value="governance">Governance</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="summary">{t('proposals.fields.summary')}</Label>
<Textarea
id="summary"
value={proposalData.summary}
onChange={(e) => setProposalData({...proposalData, summary: e.target.value})}
placeholder={t('proposals.placeholders.summary')}
rows={3}
/>
</div>
</div>
)}
{/* Step 3: Detailed Information */}
{currentStep === 3 && (
<div className="space-y-4">
<div>
<Label htmlFor="description">{t('proposals.fields.description')}</Label>
<Textarea
id="description"
value={proposalData.description}
onChange={(e) => setProposalData({...proposalData, description: e.target.value})}
placeholder={t('proposals.placeholders.description')}
rows={4}
/>
</div>
{selectedTemplate === 'treasury' && (
<div>
<Label htmlFor="budget">{t('proposals.fields.budget')}</Label>
<Input
id="budget"
type="number"
value={proposalData.budget}
onChange={(e) => setProposalData({...proposalData, budget: e.target.value})}
placeholder="Amount in PZK"
/>
</div>
)}
<div>
<Label htmlFor="timeline">{t('proposals.fields.timeline')}</Label>
<Input
id="timeline"
value={proposalData.timeline}
onChange={(e) => setProposalData({...proposalData, timeline: e.target.value})}
placeholder="e.g., 3 months"
/>
</div>
<div>
<Label>{t('proposals.fields.milestones')}</Label>
{proposalData.milestones.map((milestone, index) => (
<Input
key={index}
value={milestone}
onChange={(e) => {
const newMilestones = [...proposalData.milestones];
newMilestones[index] = e.target.value;
setProposalData({...proposalData, milestones: newMilestones});
}}
placeholder={`Milestone ${index + 1}`}
className="mb-2"
/>
))}
<Button
variant="outline"
size="sm"
onClick={() => setProposalData({...proposalData, milestones: [...proposalData.milestones, '']})}
>
Add Milestone
</Button>
</div>
</div>
)}
{/* Step 4: Impact Assessment */}
{currentStep === 4 && (
<div className="space-y-4">
<div>
<Label htmlFor="impact">{t('proposals.fields.impact')}</Label>
<Textarea
id="impact"
value={proposalData.impact}
onChange={(e) => setProposalData({...proposalData, impact: e.target.value})}
placeholder={t('proposals.placeholders.impact')}
rows={3}
/>
</div>
<div>
<Label htmlFor="metrics">{t('proposals.fields.metrics')}</Label>
<Textarea
id="metrics"
value={proposalData.metrics}
onChange={(e) => setProposalData({...proposalData, metrics: e.target.value})}
placeholder={t('proposals.placeholders.metrics')}
rows={3}
/>
</div>
<div>
<Label htmlFor="risks">{t('proposals.fields.risks')}</Label>
<Textarea
id="risks"
value={proposalData.risks}
onChange={(e) => setProposalData({...proposalData, risks: e.target.value})}
placeholder={t('proposals.placeholders.risks')}
rows={3}
/>
</div>
</div>
)}
{/* Step 5: Review */}
{currentStep === 5 && (
<div className="space-y-4">
<Alert className="border-green-200 bg-green-50">
<Check className="w-4 h-4" />
<AlertDescription>
{t('proposals.wizard.readyToSubmit')}
</AlertDescription>
</Alert>
<div className="border rounded-lg p-4 space-y-3">
<div>
<span className="font-semibold">{t('proposals.fields.title')}:</span>
<p className="text-gray-700">{proposalData.title}</p>
</div>
<div>
<span className="font-semibold">{t('proposals.fields.category')}:</span>
<p className="text-gray-700">{proposalData.category}</p>
</div>
<div>
<span className="font-semibold">{t('proposals.fields.summary')}:</span>
<p className="text-gray-700">{proposalData.summary}</p>
</div>
{proposalData.budget && (
<div>
<span className="font-semibold">{t('proposals.fields.budget')}:</span>
<p className="text-gray-700">{proposalData.budget} PZK</p>
</div>
)}
</div>
</div>
)}
{/* Navigation Buttons */}
<div className="flex justify-between pt-6">
<Button
variant="outline"
onClick={currentStep === 1 ? onCancel : handleBack}
>
<ChevronLeft className="w-4 h-4 mr-2" />
{currentStep === 1 ? t('common.cancel') : t('common.back')}
</Button>
{currentStep < steps.length ? (
<Button
onClick={handleNext}
disabled={currentStep === 1 && !selectedTemplate}
className="bg-green-600 hover:bg-green-700"
>
{t('common.next')}
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
) : (
<Button
onClick={handleSubmit}
className="bg-green-600 hover:bg-green-700"
>
<Check className="w-4 h-4 mr-2" />
{t('common.submit')}
</Button>
)}
</div>
</CardContent>
</Card>
</div>
);
};
export default ProposalWizard;
@@ -0,0 +1,237 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import { supabase } from '@/lib/supabase';
import { useToast } from '@/hooks/use-toast';
import { Shield, Save, RefreshCw, Lock, Unlock } from 'lucide-react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
interface Role {
id: string;
name: string;
description: string;
permissions: Record<string, boolean>;
is_system: boolean;
}
const PERMISSION_CATEGORIES = {
governance: {
title: 'Governance',
permissions: {
create_proposal: 'Create Proposals',
vote_proposal: 'Vote on Proposals',
delegate_vote: 'Delegate Voting Power',
manage_treasury: 'Manage Treasury',
}
},
moderation: {
title: 'Moderation',
permissions: {
moderate_content: 'Moderate Content',
ban_users: 'Ban Users',
delete_posts: 'Delete Posts',
pin_posts: 'Pin Posts',
}
},
administration: {
title: 'Administration',
permissions: {
manage_users: 'Manage Users',
manage_roles: 'Manage Roles',
view_analytics: 'View Analytics',
system_settings: 'System Settings',
}
},
security: {
title: 'Security',
permissions: {
view_audit_logs: 'View Audit Logs',
manage_sessions: 'Manage Sessions',
configure_2fa: 'Configure 2FA',
access_api: 'Access API',
}
}
};
export function PermissionEditor() {
const [roles, setRoles] = useState<Role[]>([]);
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const { toast } = useToast();
useEffect(() => {
loadRoles();
}, []);
const loadRoles = async () => {
try {
const { data, error } = await supabase
.from('roles')
.select('*')
.order('name');
if (error) throw error;
setRoles(data || []);
if (data && data.length > 0) {
setSelectedRole(data[0]);
}
} catch (error) {
console.error('Error loading roles:', error);
toast({
title: 'Error',
description: 'Failed to load roles',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const togglePermission = (category: string, permission: string) => {
if (!selectedRole || selectedRole.is_system) return;
const fullPermission = `${category}.${permission}`;
setSelectedRole({
...selectedRole,
permissions: {
...selectedRole.permissions,
[fullPermission]: !selectedRole.permissions[fullPermission]
}
});
};
const savePermissions = async () => {
if (!selectedRole) return;
setSaving(true);
try {
const { error } = await supabase
.from('roles')
.update({ permissions: selectedRole.permissions })
.eq('id', selectedRole.id);
if (error) throw error;
toast({
title: 'Success',
description: 'Permissions updated successfully',
});
} catch (error) {
toast({
title: 'Error',
description: 'Failed to save permissions',
variant: 'destructive',
});
} finally {
setSaving(false);
}
};
const resetPermissions = () => {
if (!selectedRole) return;
const original = roles.find(r => r.id === selectedRole.id);
if (original) {
setSelectedRole(original);
}
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Permission Editor
</CardTitle>
</CardHeader>
<CardContent>
<Tabs value={selectedRole?.id} onValueChange={(id) => {
const role = roles.find(r => r.id === id);
if (role) setSelectedRole(role);
}}>
<TabsList className="grid grid-cols-4 w-full">
{roles.map(role => (
<TabsTrigger key={role.id} value={role.id}>
{role.name}
{role.is_system && (
<Lock className="h-3 w-3 ml-1" />
)}
</TabsTrigger>
))}
</TabsList>
{selectedRole && (
<TabsContent value={selectedRole.id} className="space-y-6 mt-6">
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-semibold">{selectedRole.name}</h3>
<p className="text-sm text-muted-foreground">{selectedRole.description}</p>
{selectedRole.is_system && (
<Badge variant="secondary" className="mt-2">
<Lock className="h-3 w-3 mr-1" />
System Role (Read Only)
</Badge>
)}
</div>
{!selectedRole.is_system && (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={resetPermissions}
>
<RefreshCw className="h-4 w-4 mr-1" />
Reset
</Button>
<Button
size="sm"
onClick={savePermissions}
disabled={saving}
>
<Save className="h-4 w-4 mr-1" />
Save Changes
</Button>
</div>
)}
</div>
<div className="space-y-6">
{Object.entries(PERMISSION_CATEGORIES).map(([categoryKey, category]) => (
<div key={categoryKey} className="space-y-3">
<h4 className="font-medium text-sm">{category.title}</h4>
<div className="space-y-2">
{Object.entries(category.permissions).map(([permKey, permName]) => {
const fullPerm = `${categoryKey}.${permKey}`;
const isEnabled = selectedRole.permissions[fullPerm] || false;
return (
<div key={permKey} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center gap-2">
{isEnabled ? (
<Unlock className="h-4 w-4 text-green-500" />
) : (
<Lock className="h-4 w-4 text-muted-foreground" />
)}
<span className="text-sm">{permName}</span>
</div>
<Switch
checked={isEnabled}
disabled={selectedRole.is_system}
onCheckedChange={() => togglePermission(categoryKey, permKey)}
/>
</div>
);
})}
</div>
</div>
))}
</div>
</TabsContent>
)}
</Tabs>
</CardContent>
</Card>
);
}
+291
View File
@@ -0,0 +1,291 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { supabase } from '@/lib/supabase';
import { Shield, AlertTriangle, CheckCircle, XCircle, TrendingUp, Users, Key, Activity } from 'lucide-react';
import { LineChart, Line, AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
interface SecurityMetrics {
totalUsers: number;
activeUsers: number;
twoFactorEnabled: number;
suspiciousActivities: number;
failedLogins: number;
securityScore: number;
}
interface AuditLog {
id: string;
action: string;
user_id: string;
ip_address: string;
created_at: string;
severity: 'low' | 'medium' | 'high' | 'critical';
}
export function SecurityAudit() {
const [metrics, setMetrics] = useState<SecurityMetrics>({
totalUsers: 0,
activeUsers: 0,
twoFactorEnabled: 0,
suspiciousActivities: 0,
failedLogins: 0,
securityScore: 0,
});
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadSecurityData();
}, []);
const loadSecurityData = async () => {
try {
// Load user metrics
const { data: users } = await supabase
.from('profiles')
.select('id, created_at');
const { data: twoFactor } = await supabase
.from('two_factor_auth')
.select('user_id')
.eq('enabled', true);
const { data: sessions } = await supabase
.from('user_sessions')
.select('user_id')
.eq('is_active', true);
const { data: logs } = await supabase
.from('activity_logs')
.select('*')
.order('created_at', { ascending: false })
.limit(100);
// Calculate metrics
const totalUsers = users?.length || 0;
const activeUsers = sessions?.length || 0;
const twoFactorEnabled = twoFactor?.length || 0;
const suspiciousActivities = logs?.filter(l =>
l.action.includes('failed') || l.action.includes('suspicious')
).length || 0;
const failedLogins = logs?.filter(l =>
l.action === 'login_failed'
).length || 0;
// Calculate security score
const score = Math.round(
((twoFactorEnabled / Math.max(totalUsers, 1)) * 40) +
((activeUsers / Math.max(totalUsers, 1)) * 20) +
(Math.max(0, 40 - (suspiciousActivities * 2)))
);
setMetrics({
totalUsers,
activeUsers,
twoFactorEnabled,
suspiciousActivities,
failedLogins,
securityScore: score,
});
setAuditLogs(logs || []);
} catch (error) {
console.error('Error loading security data:', error);
} finally {
setLoading(false);
}
};
const getScoreColor = (score: number) => {
if (score >= 80) return 'text-green-500';
if (score >= 60) return 'text-yellow-500';
if (score >= 40) return 'text-orange-500';
return 'text-red-500';
};
const getScoreBadge = (score: number) => {
if (score >= 80) return { text: 'Excellent', variant: 'default' as const };
if (score >= 60) return { text: 'Good', variant: 'secondary' as const };
if (score >= 40) return { text: 'Fair', variant: 'outline' as const };
return { text: 'Poor', variant: 'destructive' as const };
};
const pieData = [
{ name: '2FA Enabled', value: metrics.twoFactorEnabled, color: '#10b981' },
{ name: 'No 2FA', value: metrics.totalUsers - metrics.twoFactorEnabled, color: '#ef4444' },
];
const activityData = [
{ name: 'Mon', logins: 45, failures: 2 },
{ name: 'Tue', logins: 52, failures: 3 },
{ name: 'Wed', logins: 48, failures: 1 },
{ name: 'Thu', logins: 61, failures: 4 },
{ name: 'Fri', logins: 55, failures: 2 },
{ name: 'Sat', logins: 32, failures: 1 },
{ name: 'Sun', logins: 28, failures: 0 },
];
const scoreBadge = getScoreBadge(metrics.securityScore);
return (
<div className="space-y-6">
{/* Security Score Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Security Score
</span>
<Badge variant={scoreBadge.variant}>{scoreBadge.text}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="text-center">
<div className={`text-6xl font-bold ${getScoreColor(metrics.securityScore)}`}>
{metrics.securityScore}
</div>
<p className="text-sm text-muted-foreground mt-2">Out of 100</p>
</div>
<Progress value={metrics.securityScore} className="h-3" />
</div>
</CardContent>
</Card>
{/* Metrics Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Total Users</p>
<p className="text-2xl font-bold">{metrics.totalUsers}</p>
</div>
<Users className="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">2FA Enabled</p>
<p className="text-2xl font-bold">{metrics.twoFactorEnabled}</p>
</div>
<Key className="h-8 w-8 text-green-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Active Sessions</p>
<p className="text-2xl font-bold">{metrics.activeUsers}</p>
</div>
<Activity className="h-8 w-8 text-purple-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Suspicious</p>
<p className="text-2xl font-bold">{metrics.suspiciousActivities}</p>
</div>
<AlertTriangle className="h-8 w-8 text-orange-500" />
</div>
</CardContent>
</Card>
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Login Activity</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={activityData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Area type="monotone" dataKey="logins" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.6} />
<Area type="monotone" dataKey="failures" stroke="#ef4444" fill="#ef4444" fillOpacity={0.6} />
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>2FA Adoption</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={pieData}
cx="50%"
cy="50%"
labelLine={false}
label={(entry) => `${entry.name}: ${entry.value}`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{pieData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Recent Security Events */}
<Card>
<CardHeader>
<CardTitle>Recent Security Events</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{auditLogs.slice(0, 10).map((log) => (
<div key={log.id} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex items-center gap-3">
{log.severity === 'critical' && <XCircle className="h-5 w-5 text-red-500" />}
{log.severity === 'high' && <AlertTriangle className="h-5 w-5 text-orange-500" />}
{log.severity === 'medium' && <AlertTriangle className="h-5 w-5 text-yellow-500" />}
{log.severity === 'low' && <CheckCircle className="h-5 w-5 text-green-500" />}
<div>
<p className="font-medium">{log.action}</p>
<p className="text-sm text-muted-foreground">IP: {log.ip_address}</p>
</div>
</div>
<Badge variant={
log.severity === 'critical' ? 'destructive' :
log.severity === 'high' ? 'destructive' :
log.severity === 'medium' ? 'secondary' :
'outline'
}>
{log.severity}
</Badge>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}
+152
View File
@@ -0,0 +1,152 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { supabase } from '@/lib/supabase';
import { useToast } from '@/hooks/use-toast';
import { Monitor, Shield, LogOut, AlertTriangle, Activity } from 'lucide-react';
import { format } from 'date-fns';
interface Session {
id: string;
user_id: string;
ip_address: string;
user_agent: string;
created_at: string;
last_activity: string;
is_active: boolean;
profiles: {
username: string;
email: string;
};
}
export function SessionMonitor() {
const [sessions, setSessions] = useState<Session[]>([]);
const [loading, setLoading] = useState(true);
const { toast } = useToast();
useEffect(() => {
loadSessions();
const interval = setInterval(loadSessions, 30000);
return () => clearInterval(interval);
}, []);
const loadSessions = async () => {
try {
const { data, error } = await supabase
.from('user_sessions')
.select(`
*,
profiles:user_id (username, email)
`)
.order('last_activity', { ascending: false });
if (error) throw error;
setSessions(data || []);
} catch (error) {
console.error('Error loading sessions:', error);
} finally {
setLoading(false);
}
};
const terminateSession = async (sessionId: string) => {
try {
const { error } = await supabase
.from('user_sessions')
.update({ is_active: false })
.eq('id', sessionId);
if (error) throw error;
toast({
title: 'Session Terminated',
description: 'The session has been successfully terminated.',
});
loadSessions();
} catch (error) {
toast({
title: 'Error',
description: 'Failed to terminate session',
variant: 'destructive',
});
}
};
const getDeviceInfo = (userAgent: string) => {
if (userAgent.includes('Mobile')) return 'Mobile';
if (userAgent.includes('Tablet')) return 'Tablet';
return 'Desktop';
};
const getActivityStatus = (lastActivity: string) => {
const diff = Date.now() - new Date(lastActivity).getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 5) return { text: 'Active', variant: 'default' as const };
if (minutes < 30) return { text: 'Idle', variant: 'secondary' as const };
return { text: 'Inactive', variant: 'outline' as const };
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Monitor className="h-5 w-5" />
Active Sessions
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{sessions.map((session) => {
const status = getActivityStatus(session.last_activity);
return (
<div key={session.id} className="border rounded-lg p-4">
<div className="flex justify-between items-start">
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="font-medium">{session.profiles?.username}</span>
<Badge variant={status.variant}>
<Activity className="h-3 w-3 mr-1" />
{status.text}
</Badge>
{session.is_active && (
<Badge variant="default">
<Shield className="h-3 w-3 mr-1" />
Active
</Badge>
)}
</div>
<div className="text-sm text-muted-foreground space-y-1">
<p>IP: {session.ip_address}</p>
<p>Device: {getDeviceInfo(session.user_agent)}</p>
<p>Started: {format(new Date(session.created_at), 'PPp')}</p>
<p>Last Activity: {format(new Date(session.last_activity), 'PPp')}</p>
</div>
</div>
{session.is_active && (
<Button
size="sm"
variant="destructive"
onClick={() => terminateSession(session.id)}
>
<LogOut className="h-4 w-4 mr-1" />
Terminate
</Button>
)}
</div>
</div>
);
})}
{sessions.length === 0 && !loading && (
<div className="text-center py-8 text-muted-foreground">
<Monitor className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No active sessions</p>
</div>
)}
</div>
</CardContent>
</Card>
);
}
+308
View File
@@ -0,0 +1,308 @@
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { TrendingUp, Coins, Lock, Clock, Gift, Calculator, Info } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface StakingPool {
id: string;
name: string;
token: 'HEZ' | 'PEZ';
apy: number;
totalStaked: number;
minStake: number;
lockPeriod: number;
userStaked?: number;
rewards?: number;
}
export const StakingDashboard: React.FC = () => {
const { t } = useTranslation();
const [selectedPool, setSelectedPool] = useState<StakingPool | null>(null);
const [stakeAmount, setStakeAmount] = useState('');
const [unstakeAmount, setUnstakeAmount] = useState('');
const stakingPools: StakingPool[] = [
{
id: '1',
name: 'HEZ Flexible',
token: 'HEZ',
apy: 8.5,
totalStaked: 1500000,
minStake: 100,
lockPeriod: 0,
userStaked: 5000,
rewards: 42.5
},
{
id: '2',
name: 'HEZ Locked 30 Days',
token: 'HEZ',
apy: 12.0,
totalStaked: 3200000,
minStake: 500,
lockPeriod: 30,
userStaked: 10000,
rewards: 100
},
{
id: '3',
name: 'PEZ High Yield',
token: 'PEZ',
apy: 15.5,
totalStaked: 800000,
minStake: 1000,
lockPeriod: 60,
userStaked: 0,
rewards: 0
},
{
id: '4',
name: 'PEZ Governance',
token: 'PEZ',
apy: 18.0,
totalStaked: 2100000,
minStake: 2000,
lockPeriod: 90,
userStaked: 25000,
rewards: 375
}
];
const handleStake = (pool: StakingPool) => {
console.log('Staking', stakeAmount, pool.token, 'in pool', pool.name);
// Implement staking logic
};
const handleUnstake = (pool: StakingPool) => {
console.log('Unstaking', unstakeAmount, pool.token, 'from pool', pool.name);
// Implement unstaking logic
};
const handleClaimRewards = (pool: StakingPool) => {
console.log('Claiming rewards from pool', pool.name);
// Implement claim rewards logic
};
const totalStaked = stakingPools.reduce((sum, pool) => sum + (pool.userStaked || 0), 0);
const totalRewards = stakingPools.reduce((sum, pool) => sum + (pool.rewards || 0), 0);
return (
<div className="space-y-6">
{/* Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">Total Staked</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">{totalStaked.toLocaleString()}</div>
<p className="text-xs text-gray-500 mt-1">Across all pools</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">Total Rewards</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-500">{totalRewards.toFixed(2)}</div>
<p className="text-xs text-gray-500 mt-1">Ready to claim</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">Average APY</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-yellow-500">13.5%</div>
<p className="text-xs text-gray-500 mt-1">Weighted average</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">Next Reward</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">4h 23m</div>
<p className="text-xs text-gray-500 mt-1">Distribution time</p>
</CardContent>
</Card>
</div>
{/* Staking Pools */}
<Card className="bg-gray-900 border-gray-800">
<CardHeader>
<CardTitle className="text-xl text-white">Staking Pools</CardTitle>
<CardDescription className="text-gray-400">
Choose a pool and start earning rewards
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{stakingPools.map((pool) => (
<Card key={pool.id} className="bg-gray-800 border-gray-700">
<CardHeader>
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-lg text-white">{pool.name}</CardTitle>
<Badge variant={pool.token === 'HEZ' ? 'default' : 'secondary'}>
{pool.token}
</Badge>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-green-500">{pool.apy}%</div>
<p className="text-xs text-gray-400">APY</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Total Staked</span>
<span className="text-white">{pool.totalStaked.toLocaleString()} {pool.token}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Lock Period</span>
<span className="text-white">
{pool.lockPeriod === 0 ? 'Flexible' : `${pool.lockPeriod} days`}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Min. Stake</span>
<span className="text-white">{pool.minStake} {pool.token}</span>
</div>
</div>
{pool.userStaked && pool.userStaked > 0 && (
<div className="pt-2 border-t border-gray-700">
<div className="flex justify-between text-sm mb-2">
<span className="text-gray-400">Your Stake</span>
<span className="text-white font-semibold">{pool.userStaked.toLocaleString()} {pool.token}</span>
</div>
<div className="flex justify-between text-sm mb-3">
<span className="text-gray-400">Rewards</span>
<span className="text-green-500 font-semibold">{pool.rewards?.toFixed(2)} {pool.token}</span>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => handleClaimRewards(pool)}
className="flex-1"
>
<Gift className="w-4 h-4 mr-1" />
Claim
</Button>
<Button
size="sm"
variant="secondary"
onClick={() => setSelectedPool(pool)}
className="flex-1"
>
Manage
</Button>
</div>
</div>
)}
{(!pool.userStaked || pool.userStaked === 0) && (
<Button
className="w-full bg-green-600 hover:bg-green-700"
onClick={() => setSelectedPool(pool)}
>
<Lock className="w-4 h-4 mr-2" />
Stake {pool.token}
</Button>
)}
</CardContent>
</Card>
))}
</div>
</CardContent>
</Card>
{/* Stake/Unstake Modal */}
{selectedPool && (
<Card className="bg-gray-900 border-gray-800">
<CardHeader>
<CardTitle>Manage {selectedPool.name}</CardTitle>
<CardDescription>Stake or unstake your tokens</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="stake">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="stake">Stake</TabsTrigger>
<TabsTrigger value="unstake">Unstake</TabsTrigger>
</TabsList>
<TabsContent value="stake" className="space-y-4">
<div>
<Label>Amount to Stake</Label>
<Input
type="number"
placeholder={`Min: ${selectedPool.minStake} ${selectedPool.token}`}
value={stakeAmount}
onChange={(e) => setStakeAmount(e.target.value)}
className="bg-gray-800 border-gray-700"
/>
</div>
<div className="bg-gray-800 p-4 rounded-lg space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Estimated APY</span>
<span className="text-green-500 font-semibold">{selectedPool.apy}%</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Lock Period</span>
<span className="text-white">{selectedPool.lockPeriod === 0 ? 'None' : `${selectedPool.lockPeriod} days`}</span>
</div>
</div>
<Button
className="w-full bg-green-600 hover:bg-green-700"
onClick={() => handleStake(selectedPool)}
>
Confirm Stake
</Button>
</TabsContent>
<TabsContent value="unstake" className="space-y-4">
<div>
<Label>Amount to Unstake</Label>
<Input
type="number"
placeholder={`Max: ${selectedPool.userStaked} ${selectedPool.token}`}
value={unstakeAmount}
onChange={(e) => setUnstakeAmount(e.target.value)}
className="bg-gray-800 border-gray-700"
/>
</div>
<div className="bg-yellow-900/20 border border-yellow-600/50 p-4 rounded-lg">
<p className="text-sm text-yellow-500">
<Info className="w-4 h-4 inline mr-1" />
{selectedPool.lockPeriod > 0
? `Tokens are locked for ${selectedPool.lockPeriod} days. Early withdrawal may incur penalties.`
: 'You can unstake anytime without penalties.'}
</p>
</div>
<Button
className="w-full"
variant="destructive"
onClick={() => handleUnstake(selectedPool)}
>
Confirm Unstake
</Button>
</TabsContent>
</Tabs>
</CardContent>
</Card>
)}
</div>
);
};
+69
View File
@@ -0,0 +1,69 @@
"use client"
import * as React from "react"
import { createContext, useContext, useEffect, useState } from "react"
import { ThemeProviderProps } from "next-themes/dist/types"
type Theme = "dark" | "light" | "system"
type ThemeContextType = {
theme: Theme
setTheme: (theme: Theme) => void
}
const ThemeContext = createContext<ThemeContextType | null>(null)
export function ThemeProvider({
children,
defaultTheme = "system",
value: _value,
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => {
if (typeof window !== "undefined") {
const savedTheme = localStorage.getItem("theme")
return (savedTheme && (savedTheme === "dark" || savedTheme === "light" || savedTheme === "system")
? savedTheme
: defaultTheme) as Theme
}
return defaultTheme as Theme
})
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value: ThemeContextType = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem("theme", theme)
setTheme(theme)
},
}
return (
<ThemeContext.Provider value={value} {...props}>
{children}
</ThemeContext.Provider>
)
}
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider")
}
return context
}
+296
View File
@@ -0,0 +1,296 @@
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
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 { Badge } from '@/components/ui/badge';
import { useTranslation } from 'react-i18next';
import {
Plus,
Trash2,
Calculator,
FileText,
Users,
Calendar,
DollarSign,
AlertCircle
} from 'lucide-react';
interface BudgetItem {
id: string;
description: string;
amount: number;
category: string;
justification: string;
}
interface Milestone {
id: string;
title: string;
deliverables: string;
amount: number;
deadline: string;
}
export const FundingProposal: React.FC = () => {
const { t } = useTranslation();
const [proposalTitle, setProposalTitle] = useState('');
const [proposalDescription, setProposalDescription] = useState('');
const [category, setCategory] = useState('');
const [budgetItems, setBudgetItems] = useState<BudgetItem[]>([
{ id: '1', description: '', amount: 0, category: '', justification: '' }
]);
const [milestones, setMilestones] = useState<Milestone[]>([
{ id: '1', title: '', deliverables: '', amount: 0, deadline: '' }
]);
const addBudgetItem = () => {
setBudgetItems([...budgetItems, {
id: Date.now().toString(),
description: '',
amount: 0,
category: '',
justification: ''
}]);
};
const removeBudgetItem = (id: string) => {
setBudgetItems(budgetItems.filter(item => item.id !== id));
};
const updateBudgetItem = (id: string, field: keyof BudgetItem, value: any) => {
setBudgetItems(budgetItems.map(item =>
item.id === id ? { ...item, [field]: value } : item
));
};
const addMilestone = () => {
setMilestones([...milestones, {
id: Date.now().toString(),
title: '',
deliverables: '',
amount: 0,
deadline: ''
}]);
};
const removeMilestone = (id: string) => {
setMilestones(milestones.filter(m => m.id !== id));
};
const updateMilestone = (id: string, field: keyof Milestone, value: any) => {
setMilestones(milestones.map(m =>
m.id === id ? { ...m, [field]: value } : m
));
};
const totalBudget = budgetItems.reduce((sum, item) => sum + (item.amount || 0), 0);
const totalMilestoneAmount = milestones.reduce((sum, m) => sum + (m.amount || 0), 0);
return (
<div className="space-y-6">
{/* Proposal Header */}
<Card>
<CardHeader>
<CardTitle>Create Funding Proposal</CardTitle>
<CardDescription>Submit a detailed budget request for treasury funding</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">Proposal Title</Label>
<Input
id="title"
placeholder="Enter a clear, descriptive title"
value={proposalTitle}
onChange={(e) => setProposalTitle(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="category">Category</Label>
<Select value={category} onValueChange={setCategory}>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="development">Development</SelectItem>
<SelectItem value="marketing">Marketing</SelectItem>
<SelectItem value="operations">Operations</SelectItem>
<SelectItem value="community">Community</SelectItem>
<SelectItem value="research">Research</SelectItem>
<SelectItem value="infrastructure">Infrastructure</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Provide a detailed description of the proposal"
rows={4}
value={proposalDescription}
onChange={(e) => setProposalDescription(e.target.value)}
/>
</div>
</CardContent>
</Card>
{/* Budget Items */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Budget Breakdown</span>
<Badge variant="outline" className="text-lg px-3 py-1">
Total: ${totalBudget.toLocaleString()}
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{budgetItems.map((item, index) => (
<div key={item.id} className="p-4 border rounded-lg space-y-3">
<div className="flex items-center justify-between">
<span className="font-medium">Item {index + 1}</span>
{budgetItems.length > 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => removeBudgetItem(item.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="space-y-2">
<Label>Description</Label>
<Input
placeholder="Budget item description"
value={item.description}
onChange={(e) => updateBudgetItem(item.id, 'description', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Amount ($)</Label>
<Input
type="number"
placeholder="0"
value={item.amount || ''}
onChange={(e) => updateBudgetItem(item.id, 'amount', parseFloat(e.target.value) || 0)}
/>
</div>
</div>
<div className="space-y-2">
<Label>Justification</Label>
<Textarea
placeholder="Explain why this expense is necessary"
rows={2}
value={item.justification}
onChange={(e) => updateBudgetItem(item.id, 'justification', e.target.value)}
/>
</div>
</div>
))}
<Button onClick={addBudgetItem} variant="outline" className="w-full">
<Plus className="h-4 w-4 mr-2" />
Add Budget Item
</Button>
</CardContent>
</Card>
{/* Milestones */}
<Card>
<CardHeader>
<CardTitle>Milestones & Deliverables</CardTitle>
<CardDescription>Define clear milestones with payment schedule</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{milestones.map((milestone, index) => (
<div key={milestone.id} className="p-4 border rounded-lg space-y-3">
<div className="flex items-center justify-between">
<span className="font-medium">Milestone {index + 1}</span>
{milestones.length > 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => removeMilestone(milestone.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="space-y-2">
<Label>Title</Label>
<Input
placeholder="Milestone title"
value={milestone.title}
onChange={(e) => updateMilestone(milestone.id, 'title', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Payment Amount ($)</Label>
<Input
type="number"
placeholder="0"
value={milestone.amount || ''}
onChange={(e) => updateMilestone(milestone.id, 'amount', parseFloat(e.target.value) || 0)}
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="space-y-2">
<Label>Deliverables</Label>
<Textarea
placeholder="What will be delivered"
rows={2}
value={milestone.deliverables}
onChange={(e) => updateMilestone(milestone.id, 'deliverables', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Deadline</Label>
<Input
type="date"
value={milestone.deadline}
onChange={(e) => updateMilestone(milestone.id, 'deadline', e.target.value)}
/>
</div>
</div>
</div>
))}
<Button onClick={addMilestone} variant="outline" className="w-full">
<Plus className="h-4 w-4 mr-2" />
Add Milestone
</Button>
{totalMilestoneAmount !== totalBudget && totalMilestoneAmount > 0 && (
<div className="flex items-center gap-2 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<AlertCircle className="h-5 w-5 text-yellow-600" />
<span className="text-sm">
Milestone total (${totalMilestoneAmount.toLocaleString()}) doesn't match budget total (${totalBudget.toLocaleString()})
</span>
</div>
)}
</CardContent>
</Card>
{/* Submit Button */}
<div className="flex justify-end gap-3">
<Button variant="outline">Save Draft</Button>
<Button>Submit Proposal</Button>
</div>
</div>
);
};
@@ -0,0 +1,278 @@
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useTranslation } from 'react-i18next';
import {
Shield,
CheckCircle,
XCircle,
Clock,
Users,
AlertTriangle,
FileText,
DollarSign
} from 'lucide-react';
interface Approval {
id: string;
proposalTitle: string;
amount: number;
category: string;
requester: string;
description: string;
requiredSignatures: number;
currentSignatures: number;
signers: Array<{
name: string;
status: 'approved' | 'rejected' | 'pending';
timestamp?: string;
comment?: string;
}>;
deadline: string;
status: 'pending' | 'approved' | 'rejected' | 'expired';
}
export const MultiSigApproval: React.FC = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('pending');
const [approvals] = useState<Approval[]>([
{
id: '1',
proposalTitle: 'Infrastructure Upgrade - Q1 2024',
amount: 45000,
category: 'Infrastructure',
requester: 'Tech Team',
description: 'Upgrade cloud infrastructure for improved performance',
requiredSignatures: 3,
currentSignatures: 1,
signers: [
{ name: 'Alice', status: 'approved', timestamp: '2024-01-08 14:30', comment: 'Looks good' },
{ name: 'Bob', status: 'pending' },
{ name: 'Charlie', status: 'pending' },
{ name: 'Diana', status: 'pending' }
],
deadline: '2024-01-20',
status: 'pending'
},
{
id: '2',
proposalTitle: 'Developer Grants Program',
amount: 100000,
category: 'Development',
requester: 'Dev Relations',
description: 'Fund developer grants for ecosystem growth',
requiredSignatures: 4,
currentSignatures: 2,
signers: [
{ name: 'Alice', status: 'approved', timestamp: '2024-01-07 10:15' },
{ name: 'Bob', status: 'approved', timestamp: '2024-01-07 11:45' },
{ name: 'Charlie', status: 'pending' },
{ name: 'Diana', status: 'pending' },
{ name: 'Eve', status: 'pending' }
],
deadline: '2024-01-25',
status: 'pending'
}
]);
const pendingApprovals = approvals.filter(a => a.status === 'pending');
const approvedApprovals = approvals.filter(a => a.status === 'approved');
const rejectedApprovals = approvals.filter(a => a.status === 'rejected');
const getStatusIcon = (status: string) => {
switch (status) {
case 'approved':
return <CheckCircle className="h-4 w-4 text-green-500" />;
case 'rejected':
return <XCircle className="h-4 w-4 text-red-500" />;
case 'pending':
return <Clock className="h-4 w-4 text-yellow-500" />;
default:
return null;
}
};
const ApprovalCard = ({ approval }: { approval: Approval }) => {
const progress = (approval.currentSignatures / approval.requiredSignatures) * 100;
return (
<Card>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">{approval.proposalTitle}</CardTitle>
<CardDescription>{approval.description}</CardDescription>
</div>
<Badge variant="outline">{approval.category}</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<DollarSign className="h-4 w-4 text-muted-foreground" />
<span className="font-semibold">${approval.amount.toLocaleString()}</span>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="h-4 w-4" />
<span>Deadline: {approval.deadline}</span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span>Approval Progress</span>
<span className="font-medium">
{approval.currentSignatures}/{approval.requiredSignatures} signatures
</span>
</div>
<Progress value={progress} className="h-2" />
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Signers</p>
<div className="flex flex-wrap gap-2">
{approval.signers.map((signer, index) => (
<div key={index} className="flex items-center gap-2">
<Avatar className="h-8 w-8">
<AvatarFallback className="text-xs">
{signer.name[0]}
</AvatarFallback>
</Avatar>
<div className="flex items-center gap-1">
<span className="text-sm">{signer.name}</span>
{getStatusIcon(signer.status)}
</div>
</div>
))}
</div>
</div>
<div className="flex gap-2">
<Button className="flex-1" size="sm">
<CheckCircle className="h-4 w-4 mr-2" />
Approve
</Button>
<Button variant="outline" className="flex-1" size="sm">
<XCircle className="h-4 w-4 mr-2" />
Reject
</Button>
<Button variant="ghost" size="sm">
View Details
</Button>
</div>
</CardContent>
</Card>
);
};
return (
<div className="space-y-6">
{/* Stats Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Pending Approvals</p>
<p className="text-2xl font-bold">{pendingApprovals.length}</p>
</div>
<Clock className="h-8 w-8 text-yellow-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Total Value</p>
<p className="text-2xl font-bold">
${(pendingApprovals.reduce((sum, a) => sum + a.amount, 0) / 1000).toFixed(0)}k
</p>
</div>
<DollarSign className="h-8 w-8 text-green-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Active Signers</p>
<p className="text-2xl font-bold">5</p>
</div>
<Users className="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Expiring Soon</p>
<p className="text-2xl font-bold">2</p>
</div>
<AlertTriangle className="h-8 w-8 text-orange-500" />
</div>
</CardContent>
</Card>
</div>
{/* Approvals Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="pending">
Pending ({pendingApprovals.length})
</TabsTrigger>
<TabsTrigger value="approved">
Approved ({approvedApprovals.length})
</TabsTrigger>
<TabsTrigger value="rejected">
Rejected ({rejectedApprovals.length})
</TabsTrigger>
</TabsList>
<TabsContent value="pending" className="space-y-4">
{pendingApprovals.map(approval => (
<ApprovalCard key={approval.id} approval={approval} />
))}
</TabsContent>
<TabsContent value="approved" className="space-y-4">
{approvedApprovals.length === 0 ? (
<Card>
<CardContent className="p-6 text-center text-muted-foreground">
No approved proposals yet
</CardContent>
</Card>
) : (
approvedApprovals.map(approval => (
<ApprovalCard key={approval.id} approval={approval} />
))
)}
</TabsContent>
<TabsContent value="rejected" className="space-y-4">
{rejectedApprovals.length === 0 ? (
<Card>
<CardContent className="p-6 text-center text-muted-foreground">
No rejected proposals
</CardContent>
</Card>
) : (
rejectedApprovals.map(approval => (
<ApprovalCard key={approval.id} approval={approval} />
))
)}
</TabsContent>
</Tabs>
</div>
);
};
+296
View File
@@ -0,0 +1,296 @@
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useTranslation } from 'react-i18next';
import {
Download,
Filter,
Search,
ArrowUpDown,
FileText,
CheckCircle,
XCircle,
Clock,
TrendingUp,
TrendingDown
} from 'lucide-react';
interface Transaction {
id: string;
date: string;
description: string;
category: string;
amount: number;
status: 'completed' | 'pending' | 'rejected';
proposalId: string;
recipient: string;
approvers: string[];
}
export const SpendingHistory: React.FC = () => {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
const [filterCategory, setFilterCategory] = useState('all');
const [filterStatus, setFilterStatus] = useState('all');
const [sortBy, setSortBy] = useState('date');
const [transactions] = useState<Transaction[]>([
{
id: '1',
date: '2024-01-15',
description: 'Q1 Development Team Salaries',
category: 'Development',
amount: 85000,
status: 'completed',
proposalId: 'PROP-001',
recipient: 'Dev Team Multisig',
approvers: ['Alice', 'Bob', 'Charlie']
},
{
id: '2',
date: '2024-01-10',
description: 'Marketing Campaign - Social Media',
category: 'Marketing',
amount: 25000,
status: 'completed',
proposalId: 'PROP-002',
recipient: 'Marketing Agency',
approvers: ['Alice', 'Diana']
},
{
id: '3',
date: '2024-01-08',
description: 'Infrastructure Upgrade - Servers',
category: 'Infrastructure',
amount: 45000,
status: 'pending',
proposalId: 'PROP-003',
recipient: 'Cloud Provider',
approvers: ['Bob']
},
{
id: '4',
date: '2024-01-05',
description: 'Community Hackathon Prizes',
category: 'Community',
amount: 15000,
status: 'completed',
proposalId: 'PROP-004',
recipient: 'Hackathon Winners',
approvers: ['Alice', 'Bob', 'Eve']
},
{
id: '5',
date: '2024-01-03',
description: 'Research Grant - DeFi Protocol',
category: 'Research',
amount: 50000,
status: 'rejected',
proposalId: 'PROP-005',
recipient: 'Research Lab',
approvers: []
}
]);
const getStatusIcon = (status: string) => {
switch (status) {
case 'completed':
return <CheckCircle className="h-4 w-4 text-green-500" />;
case 'pending':
return <Clock className="h-4 w-4 text-yellow-500" />;
case 'rejected':
return <XCircle className="h-4 w-4 text-red-500" />;
default:
return null;
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'completed':
return <Badge className="bg-green-100 text-green-800">Completed</Badge>;
case 'pending':
return <Badge className="bg-yellow-100 text-yellow-800">Pending</Badge>;
case 'rejected':
return <Badge className="bg-red-100 text-red-800">Rejected</Badge>;
default:
return <Badge>{status}</Badge>;
}
};
const filteredTransactions = transactions.filter(tx => {
const matchesSearch = tx.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
tx.recipient.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = filterCategory === 'all' || tx.category === filterCategory;
const matchesStatus = filterStatus === 'all' || tx.status === filterStatus;
return matchesSearch && matchesCategory && matchesStatus;
});
const totalSpent = transactions
.filter(tx => tx.status === 'completed')
.reduce((sum, tx) => sum + tx.amount, 0);
const pendingAmount = transactions
.filter(tx => tx.status === 'pending')
.reduce((sum, tx) => sum + tx.amount, 0);
return (
<div className="space-y-6">
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Total Spent (YTD)</p>
<p className="text-2xl font-bold">${(totalSpent / 1000).toFixed(0)}k</p>
</div>
<TrendingUp className="h-8 w-8 text-green-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Pending Approvals</p>
<p className="text-2xl font-bold">${(pendingAmount / 1000).toFixed(0)}k</p>
</div>
<Clock className="h-8 w-8 text-yellow-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Transactions</p>
<p className="text-2xl font-bold">{transactions.length}</p>
</div>
<FileText className="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
</div>
{/* Filters and Search */}
<Card>
<CardHeader>
<CardTitle>Transaction History</CardTitle>
<CardDescription>View and export treasury spending records</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col md:flex-row gap-4 mb-6">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search transactions..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<Select value={filterCategory} onValueChange={setFilterCategory}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
<SelectItem value="Development">Development</SelectItem>
<SelectItem value="Marketing">Marketing</SelectItem>
<SelectItem value="Infrastructure">Infrastructure</SelectItem>
<SelectItem value="Community">Community</SelectItem>
<SelectItem value="Research">Research</SelectItem>
</SelectContent>
</Select>
<Select value={filterStatus} onValueChange={setFilterStatus}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="rejected">Rejected</SelectItem>
</SelectContent>
</Select>
<Button variant="outline">
<Download className="h-4 w-4 mr-2" />
Export CSV
</Button>
</div>
{/* Transactions Table */}
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Description</TableHead>
<TableHead>Category</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Status</TableHead>
<TableHead>Approvers</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredTransactions.map((tx) => (
<TableRow key={tx.id}>
<TableCell className="font-medium">{tx.date}</TableCell>
<TableCell>
<div>
<p className="font-medium">{tx.description}</p>
<p className="text-sm text-muted-foreground">{tx.recipient}</p>
</div>
</TableCell>
<TableCell>
<Badge variant="outline">{tx.category}</Badge>
</TableCell>
<TableCell className="font-semibold">
${tx.amount.toLocaleString()}
</TableCell>
<TableCell>{getStatusBadge(tx.status)}</TableCell>
<TableCell>
<div className="flex -space-x-2">
{tx.approvers.slice(0, 3).map((approver, i) => (
<div
key={i}
className="h-8 w-8 rounded-full bg-primary/10 border-2 border-background flex items-center justify-center text-xs font-medium"
title={approver}
>
{approver[0]}
</div>
))}
{tx.approvers.length > 3 && (
<div className="h-8 w-8 rounded-full bg-muted border-2 border-background flex items-center justify-center text-xs">
+{tx.approvers.length - 3}
</div>
)}
</div>
</TableCell>
<TableCell>
<Button variant="ghost" size="sm">View</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
);
};
@@ -0,0 +1,199 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useTranslation } from 'react-i18next';
import {
DollarSign,
TrendingUp,
TrendingDown,
PieChart,
Activity,
AlertCircle,
CheckCircle,
Clock,
ArrowUpRight,
ArrowDownRight
} from 'lucide-react';
interface TreasuryMetrics {
totalBalance: number;
monthlyIncome: number;
monthlyExpenses: number;
pendingProposals: number;
approvedBudget: number;
healthScore: number;
}
interface BudgetCategory {
id: string;
name: string;
allocated: number;
spent: number;
remaining: number;
color: string;
}
export const TreasuryOverview: React.FC = () => {
const { t } = useTranslation();
const [metrics, setMetrics] = useState<TreasuryMetrics>({
totalBalance: 2500000,
monthlyIncome: 150000,
monthlyExpenses: 120000,
pendingProposals: 8,
approvedBudget: 1800000,
healthScore: 85
});
const [categories] = useState<BudgetCategory[]>([
{ id: '1', name: 'Development', allocated: 500000, spent: 320000, remaining: 180000, color: 'bg-blue-500' },
{ id: '2', name: 'Marketing', allocated: 200000, spent: 150000, remaining: 50000, color: 'bg-purple-500' },
{ id: '3', name: 'Operations', allocated: 300000, spent: 180000, remaining: 120000, color: 'bg-green-500' },
{ id: '4', name: 'Community', allocated: 150000, spent: 80000, remaining: 70000, color: 'bg-yellow-500' },
{ id: '5', name: 'Research', allocated: 250000, spent: 100000, remaining: 150000, color: 'bg-pink-500' },
{ id: '6', name: 'Infrastructure', allocated: 400000, spent: 350000, remaining: 50000, color: 'bg-indigo-500' }
]);
const getHealthStatus = (score: number) => {
if (score >= 80) return { label: 'Excellent', color: 'text-green-500', icon: CheckCircle };
if (score >= 60) return { label: 'Good', color: 'text-blue-500', icon: Activity };
if (score >= 40) return { label: 'Fair', color: 'text-yellow-500', icon: AlertCircle };
return { label: 'Critical', color: 'text-red-500', icon: AlertCircle };
};
const healthStatus = getHealthStatus(metrics.healthScore);
const HealthIcon = healthStatus.icon;
return (
<div className="space-y-6">
{/* Treasury Health Score */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Treasury Health</span>
<HealthIcon className={`h-6 w-6 ${healthStatus.color}`} />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-2xl font-bold">{metrics.healthScore}%</span>
<Badge className={healthStatus.color}>{healthStatus.label}</Badge>
</div>
<Progress value={metrics.healthScore} className="h-3" />
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-muted-foreground">Runway</p>
<p className="font-semibold">20.8 months</p>
</div>
<div>
<p className="text-muted-foreground">Burn Rate</p>
<p className="font-semibold">$120k/month</p>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Total Balance</p>
<p className="text-2xl font-bold">${(metrics.totalBalance / 1000000).toFixed(2)}M</p>
<p className="text-xs text-green-500 flex items-center mt-1">
<ArrowUpRight className="h-3 w-3 mr-1" />
+12.5% this month
</p>
</div>
<DollarSign className="h-8 w-8 text-green-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Monthly Income</p>
<p className="text-2xl font-bold">${(metrics.monthlyIncome / 1000).toFixed(0)}k</p>
<p className="text-xs text-green-500 flex items-center mt-1">
<TrendingUp className="h-3 w-3 mr-1" />
+8.3% vs last month
</p>
</div>
<TrendingUp className="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Monthly Expenses</p>
<p className="text-2xl font-bold">${(metrics.monthlyExpenses / 1000).toFixed(0)}k</p>
<p className="text-xs text-red-500 flex items-center mt-1">
<ArrowDownRight className="h-3 w-3 mr-1" />
-5.2% vs last month
</p>
</div>
<TrendingDown className="h-8 w-8 text-red-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Pending Proposals</p>
<p className="text-2xl font-bold">{metrics.pendingProposals}</p>
<p className="text-xs text-yellow-500 flex items-center mt-1">
<Clock className="h-3 w-3 mr-1" />
$450k requested
</p>
</div>
<Clock className="h-8 w-8 text-yellow-500" />
</div>
</CardContent>
</Card>
</div>
{/* Budget Categories */}
<Card>
<CardHeader>
<CardTitle>Budget Allocation by Category</CardTitle>
<CardDescription>Current quarter budget utilization</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{categories.map((category) => {
const utilization = (category.spent / category.allocated) * 100;
return (
<div key={category.id} className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">{category.name}</span>
<div className="flex items-center gap-4">
<span className="text-muted-foreground">
${(category.spent / 1000).toFixed(0)}k / ${(category.allocated / 1000).toFixed(0)}k
</span>
<Badge variant={utilization > 80 ? 'destructive' : 'secondary'}>
{utilization.toFixed(0)}%
</Badge>
</div>
</div>
<Progress value={utilization} className="h-2" />
</div>
);
})}
</div>
</CardContent>
</Card>
</div>
);
};
+56
View File
@@ -0,0 +1,56 @@
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 border-border/50", 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:text-primary [&[data-state=open]>svg]:rotate-180 [&[data-state=open]]:text-primary",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-300 ease-in-out text-muted-foreground" />
</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 text-muted-foreground 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 }
+139
View File
@@ -0,0 +1,139 @@
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-background/80 backdrop-blur-sm 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-card 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 mt-4",
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 text-primary/90", 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 mt-2", 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,
}
+65
View File
@@ -0,0 +1,65 @@
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 shadow-sm",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
success:
"border-green-500/50 text-green-600 dark:text-green-400 [&>svg]:text-green-600 dark:[&>svg]:text-green-400 bg-green-50 dark:bg-green-950/20",
warning:
"border-yellow-500/50 text-yellow-600 dark:text-yellow-400 [&>svg]:text-yellow-600 dark:[&>svg]:text-yellow-400 bg-yellow-50 dark:bg-yellow-950/20",
info:
"border-primary/50 text-primary dark:text-primary-foreground [&>svg]:text-primary bg-primary/10 dark:bg-primary/20",
},
},
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 opacity-90", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }
+5
View File
@@ -0,0 +1,5 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }
+54
View File
@@ -0,0 +1,54 @@
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> & {
size?: "sm" | "md" | "lg" | "xl"
}
>(({ className, size = "md", ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex shrink-0 overflow-hidden rounded-full border border-border/30 ring-offset-background",
size === "sm" && "h-8 w-8",
size === "md" && "h-10 w-10",
size === "lg" && "h-12 w-12",
size === "xl" && "h-16 w-16",
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 object-cover", 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 text-muted-foreground",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }
+48
View File
@@ -0,0 +1,48 @@
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-all focus:outline-none focus:ring-2 focus:ring-primary 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 border-border",
success:
"border-transparent bg-green-500/20 text-green-700 dark:text-green-300 border-green-500/30",
warning:
"border-transparent bg-yellow-500/20 text-yellow-700 dark:text-yellow-300 border-yellow-500/30",
info:
"border-transparent bg-primary/10 text-primary border-primary/30",
},
size: {
default: "px-2.5 py-0.5 text-xs",
sm: "px-2 py-0.5 text-[10px]",
lg: "px-3 py-0.5 text-sm",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, size, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant, size }), className)} {...props} />
)
}
export { Badge, badgeVariants }
+115
View 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-primary focus-visible:text-primary", 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-medium text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5 text-muted-foreground/50", className)}
{...props}
>
{children ?? <ChevronRight className="h-3.5 w-3.5" />}
</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 text-muted-foreground", 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
View 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 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",
{
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 }
+64
View File
@@ -0,0 +1,64 @@
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 text-foreground",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline", size: "sm" }),
"h-7 w-7 bg-transparent p-0 opacity-70 hover:opacity-100 transition-opacity"
),
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", size: "sm" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100 hover:text-accent-foreground"
),
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 rounded-md transition-colors",
day_today: "bg-accent/50 text-accent-foreground rounded-md",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/30 aria-selected:text-muted-foreground aria-selected:opacity-40",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent/60 aria-selected:text-accent-foreground rounded-none",
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
View 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 border-border/40 bg-background 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<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight text-foreground",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
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 }
+260
View File
@@ -0,0 +1,260 @@
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 border border-border/40 opacity-80 hover:opacity-100 transition-opacity",
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 border border-border/40 opacity-80 hover:opacity-100 transition-opacity",
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,
}
+363
View File
@@ -0,0 +1,363 @@
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/40 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border/60 [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border/40 [&_.recharts-radial-bar-background-sector]:fill-muted/50 [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted/80 [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border/40 [&_.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/95 backdrop-blur-sm 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,
}
+28
View File
@@ -0,0 +1,28 @@
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/60 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground transition-colors duration-200",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-3.5 w-3.5 transition-transform duration-200" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }
+9
View File
@@ -0,0 +1,9 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
+151
View File
@@ -0,0 +1,151 @@
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 border-border/40 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/60 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 text-muted-foreground"
{...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/60", 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 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/60 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 transition-colors",
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/70",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}
+198
View File
@@ -0,0 +1,198 @@
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/60 focus:text-accent-foreground data-[state=open]:bg-accent/60 data-[state=open]:text-accent-foreground transition-colors",
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 border-border/40 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 duration-200",
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 border-border/40 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 duration-200",
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/60 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 transition-colors",
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/60 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 transition-colors",
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 text-primary" />
</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/60 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 transition-colors",
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 text-primary" />
</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-medium 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/60", 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/70",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}
+120
View File
@@ -0,0 +1,120 @@
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-background/80 backdrop-blur-sm 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 border-border/40 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 text-foreground",
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,
}
+116
View File
@@ -0,0 +1,116 @@
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-background/80 backdrop-blur-sm", 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 border-border bg-card shadow-lg",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted/50" />
{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 text-primary/90",
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 mt-2", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
+198
View File
@@ -0,0 +1,198 @@
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 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent/60 focus:text-accent-foreground data-[state=open]:bg-accent/60 data-[state=open]:text-accent-foreground transition-colors",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</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 border-border/40 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 duration-200",
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 border-border/40 bg-popover/95 backdrop-blur-sm 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 duration-200",
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 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent/60 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
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/60 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 text-primary" />
</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/60 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 text-primary" />
</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-medium text-foreground/80",
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-border/60", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground/70", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
+176
View File
@@ -0,0 +1,176 @@
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 mb-4", 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", "text-sm font-medium mb-1", 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/80 mt-1", 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 mt-1", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}
+27
View File
@@ -0,0 +1,27 @@
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 border-border/40 bg-popover/95 backdrop-blur-sm 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 duration-200",
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }
+69
View File
@@ -0,0 +1,69 @@
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 bg-background/50 text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-1 ring-primary ring-offset-background border-primary/50",
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-primary duration-700" />
</div>
)}
</div>
)
})
InputOTPSlot.displayName = "InputOTPSlot"
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props} className="text-muted-foreground">
<Dot className="h-4 w-4" />
</div>
))
InputOTPSeparator.displayName = "InputOTPSeparator"
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
+24
View File
@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
type InputProps = React.InputHTMLAttributes<HTMLInputElement>
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-border/60 bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium 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",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
+43
View File
@@ -0,0 +1,43 @@
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",
{
variants: {
variant: {
default: "text-foreground",
muted: "text-muted-foreground",
accent: "text-primary",
},
size: {
default: "text-sm",
xs: "text-xs",
sm: "text-sm",
lg: "text-base",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, variant, size, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants({ variant, size }), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
+234
View File
@@ -0,0 +1,234 @@
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 border-border/50 bg-background/50 p-1 shadow-sm",
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/60 focus:text-accent-foreground data-[state=open]:bg-accent/60 data-[state=open]:text-accent-foreground transition-colors",
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/60 focus:text-accent-foreground data-[state=open]:bg-accent/60 data-[state=open]:text-accent-foreground transition-colors",
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 border-border/40 bg-popover/95 backdrop-blur-sm 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 duration-200",
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 border-border/40 bg-popover/95 backdrop-blur-sm 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 duration-200",
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/60 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 transition-colors",
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/60 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 transition-colors",
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 text-primary" />
</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/60 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 transition-colors",
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 text-primary" />
</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-medium text-foreground/80",
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-border/60", 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/70",
className
)}
{...props}
/>
)
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}
+128
View 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/50 px-4 py-2 text-sm font-medium transition-all hover:bg-accent/50 hover:text-accent-foreground focus:bg-accent/50 focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/60 data-[state=open]:bg-accent/60"
)
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-300 ease-in-out 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 duration-200 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 border-border/40 bg-popover/95 backdrop-blur-sm 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-primary/20 shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}
+119
View File
@@ -0,0 +1,119 @@
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,
}),
isActive && "border-primary/50 bg-primary/5 text-primary hover:bg-primary/10",
"transition-colors",
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 hover:text-primary", 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 hover:text-primary", 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 text-muted-foreground", 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,
}
+29
View File
@@ -0,0 +1,29 @@
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 border-border/40 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 duration-200",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }
+37
View File
@@ -0,0 +1,37 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
interface ProgressProps extends
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
variant?: "default" | "success" | "warning" | "error"
}
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
ProgressProps
>(({ className, value, variant = "default", ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-secondary/40",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className={cn(
"h-full w-full flex-1 transition-all duration-300 ease-in-out",
variant === "default" && "bg-primary",
variant === "success" && "bg-green-500",
variant === "warning" && "bg-yellow-500",
variant === "error" && "bg-destructive",
)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }
+42
View File
@@ -0,0 +1,42 @@
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/60 text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current animate-in scale-in-0 duration-200" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }
+43
View File
@@ -0,0 +1,43 @@
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/50 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-primary 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 border-border/50 bg-border/30 hover:bg-border/50 transition-colors">
<GripVertical className="h-2.5 w-2.5 text-primary/40" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
+48
View File
@@ -0,0 +1,48 @@
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> & {
hideScrollbar?: boolean
}
>(({ className, children, hideScrollbar = false, ...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>
{!hideScrollbar && <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 duration-300",
orientation === "vertical" &&
"h-full w-2 border-l border-l-transparent p-[1px] hover:w-2.5",
orientation === "horizontal" &&
"h-2 flex-col border-t border-t-transparent p-[1px] hover:h-2.5",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border/50 hover:bg-border/80 transition-colors" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }
+158
View File
@@ -0,0 +1,158 @@
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/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary focus-visible:border-primary/50 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 transition-colors",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50 transition-transform duration-200 ease-in-out group-data-[state=open]:rotate-180" />
</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 text-muted-foreground",
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 text-muted-foreground",
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 border-border/40 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 duration-200",
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-medium text-muted-foreground", 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/50 focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 transition-colors",
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 text-primary" />
</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,
}
+37
View File
@@ -0,0 +1,37 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
interface SeparatorProps extends
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> {
variant?: "default" | "muted" | "accent"
}
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
SeparatorProps
>(
(
{ className, orientation = "horizontal", decorative = true, variant = "default", ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0",
variant === "default" && "bg-border",
variant === "muted" && "bg-muted",
variant === "accent" && "bg-primary/30",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }
+131
View File
@@ -0,0 +1,131 @@
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "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-background/80 backdrop-blur-sm 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-card border 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 rounded-b-xl data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t rounded-t-xl 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 hover:text-primary focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:pointer-events-none">
<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 mt-4",
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-primary/90", 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 mt-2", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet, SheetClose,
SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger
}
+738
View File
@@ -0,0 +1,738 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority"
import { PanelLeft } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { Sheet, SheetContent } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar:state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
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)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
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)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// 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}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
className
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
}
)
SidebarProvider.displayName = "SidebarProvider"
const sidebarVariants = cva(
"h-full bg-background/80 backdrop-blur-sm border-r border-border/40 shadow-sm",
{
variants: {
size: {
sm: "w-16",
md: "w-64",
lg: "w-80",
},
collapsible: {
true: "transition-all duration-300 ease-in-out",
},
},
defaultVariants: {
size: "md",
},
}
)
interface SidebarProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof sidebarVariants> {
collapsed?: boolean
}
const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
(
{ className, size, collapsible, collapsed = false, children, ...props },
ref
) => {
const actualSize = collapsed ? "sm" : size
return (
<div
ref={ref}
className={cn(sidebarVariants({ size: actualSize, collapsible }), className)}
{...props}
>
{children}
</div>
)
}
)
Sidebar.displayName = "Sidebar"
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("p-4 border-b border-border/40", className)}
{...props}
/>
))
SidebarHeader.displayName = "SidebarHeader"
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("p-4 border-t border-border/40 mt-auto", className)}
{...props}
/>
))
SidebarFooter.displayName = "SidebarFooter"
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col flex-1 p-2", className)} {...props} />
))
SidebarContent.displayName = "SidebarContent"
const SidebarNav = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<nav
ref={ref}
className={cn("flex flex-col gap-1", className)}
{...props}
/>
))
SidebarNav.displayName = "SidebarNav"
const SidebarNavItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & { active?: boolean }
>(({ className, active, ...props }, ref) => (
<div
ref={ref}
className={cn(
"flex items-center px-3 py-2 rounded-md text-sm text-foreground/80 hover:text-foreground hover:bg-accent/50 transition-colors cursor-pointer",
active && "bg-accent/60 text-primary font-medium",
className
)}
{...props}
/>
))
SidebarNavItem.displayName = "SidebarNavItem"
const SidebarSection = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("mb-2", className)} {...props} />
))
SidebarSection.displayName = "SidebarSection"
const SidebarSectionTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-xs uppercase font-medium text-muted-foreground/70 tracking-wider px-3 py-1", className)}
{...props}
/>
))
SidebarSectionTitle.displayName = "SidebarSectionTitle"
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
})
SidebarTrigger.displayName = "SidebarTrigger"
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button">
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
})
SidebarRail.displayName = "SidebarRail"
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"
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className
)}
{...props}
/>
)
})
SidebarInput.displayName = "SidebarInput"
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 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 SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarGroupAction.displayName = "SidebarGroupAction"
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 (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
)
SidebarMenuButton.displayName = "SidebarMenuButton"
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className
)}
{...props}
/>
)
})
SidebarMenuAction.displayName = "SidebarMenuAction"
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuBadge.displayName = "SidebarMenuBadge"
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 flex-1 max-w-[--skeleton-width]"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
})
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuSub.displayName = "SidebarMenuSub"
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ ...props }, ref) => <li ref={ref} {...props} />)
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
SidebarNav,
SidebarNavItem,
SidebarSection,
SidebarSectionTitle
}
+25
View File
@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
animated?: boolean
}
function Skeleton({
className,
animated = true,
...props
}: SkeletonProps) {
return (
<div
className={cn(
"rounded-md bg-muted/70",
animated && "animate-pulse",
className
)}
{...props}
/>
)
}
export { Skeleton }
+26
View File
@@ -0,0 +1,26 @@
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-1.5 w-full grow overflow-hidden rounded-full bg-secondary/50">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow-sm ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:border-primary hover:scale-110" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }
+31
View File
@@ -0,0 +1,31 @@
import React from "react"
import { Toaster as Sonner, toast } from "sonner"
import { useTheme } from "@/components/theme-provider"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme } = 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, toast }
+27
View File
@@ -0,0 +1,27 @@
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-5 w-10 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary 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-4 w-4 rounded-full bg-background shadow-md ring-0 transition-transform duration-200 ease-in-out data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0.5 data-[state=checked]:bg-white"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
+117
View 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,
}
+53
View File
@@ -0,0 +1,53 @@
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/50 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-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-sm hover:text-foreground",
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-primary focus-visible:ring-offset-2 animate-in fade-in-0 data-[state=inactive]:animate-out data-[state=inactive]:fade-out-0 duration-200",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
+22
View File
@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.TextareaHTMLAttributes<HTMLTextAreaElement>
>(({ 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-sm ring-offset-background placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary focus-visible:border-primary/50 disabled:cursor-not-allowed disabled:opacity-50 transition-colors resize-none",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }
+127
View File
@@ -0,0 +1,127 @@
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,
}
+33
View File
@@ -0,0 +1,33 @@
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>
)
}
+67
View File
@@ -0,0 +1,67 @@
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> & {
orientation?: "horizontal" | "vertical"
}
>(({ className, variant, size, orientation = "horizontal", children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn(
"flex items-center gap-1",
orientation === "vertical" ? "flex-col" : "flex-row",
variant === "outline" && "bg-background rounded-md border border-input p-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,
}),
context.variant === "outline" && "data-[state=on]:bg-background data-[state=on]:text-foreground",
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }
+45
View File
@@ -0,0 +1,45 @@
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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent/60 data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent hover:bg-muted/60 hover:text-foreground",
outline:
"border border-input bg-transparent hover:bg-accent/20 hover:text-accent-foreground data-[state=on]:border-accent",
soft:
"bg-transparent hover:bg-primary/10 data-[state=on]:bg-primary/20 data-[state=on]:text-primary",
},
size: {
default: "h-10 px-3",
sm: "h-8 px-2.5 text-xs",
lg: "h-11 px-5",
},
},
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 }
+28
View File
@@ -0,0 +1,28 @@
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 border-border/40 bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-lg 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 }
+3
View File
@@ -0,0 +1,3 @@
import { useToast, toast } from "@/hooks/use-toast";
export { useToast, toast };
+176
View File
@@ -0,0 +1,176 @@
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Shield, Users, Key, CheckCircle, XCircle, Clock, Send } from 'lucide-react';
interface MultiSigTransaction {
id: string;
to: string;
amount: number;
token: string;
description: string;
requiredSignatures: number;
currentSignatures: number;
status: 'pending' | 'executed' | 'rejected';
signers: string[];
}
export const MultiSigWallet: React.FC = () => {
const [amount, setAmount] = useState('');
const [recipient, setRecipient] = useState('');
const [description, setDescription] = useState('');
const transactions: MultiSigTransaction[] = [
{
id: '1',
to: '0x742d...29Bb',
amount: 5000,
token: 'HEZ',
description: 'Development fund payment',
requiredSignatures: 3,
currentSignatures: 2,
status: 'pending',
signers: ['Alice', 'Bob']
},
{
id: '2',
to: '0x891a...45Cc',
amount: 10000,
token: 'PEZ',
description: 'Marketing campaign',
requiredSignatures: 3,
currentSignatures: 3,
status: 'executed',
signers: ['Alice', 'Bob', 'Charlie']
}
];
const handleCreateTransaction = () => {
console.log('Creating multi-sig transaction:', { amount, recipient, description });
};
const handleSign = (txId: string) => {
console.log('Signing transaction:', txId);
};
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">Wallet Balance</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">50,000 HEZ</div>
<p className="text-xs text-gray-500 mt-1">25,000 PEZ</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">Required Signatures</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">3 of 5</div>
<p className="text-xs text-gray-500 mt-1">Signers required</p>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-gray-400">Pending Transactions</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-yellow-500">2</div>
<p className="text-xs text-gray-500 mt-1">Awaiting signatures</p>
</CardContent>
</Card>
</div>
<Card className="bg-gray-900 border-gray-800">
<CardHeader>
<CardTitle>Create Transaction</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Recipient Address</Label>
<Input
placeholder="0x..."
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
className="bg-gray-800 border-gray-700"
/>
</div>
<div>
<Label>Amount</Label>
<Input
type="number"
placeholder="0"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="bg-gray-800 border-gray-700"
/>
</div>
</div>
<div>
<Label>Description</Label>
<Input
placeholder="Transaction purpose"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="bg-gray-800 border-gray-700"
/>
</div>
<Button onClick={handleCreateTransaction} className="bg-green-600 hover:bg-green-700">
<Send className="w-4 h-4 mr-2" />
Create Transaction
</Button>
</CardContent>
</Card>
<Card className="bg-gray-900 border-gray-800">
<CardHeader>
<CardTitle>Pending Transactions</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{transactions.map((tx) => (
<Card key={tx.id} className="bg-gray-800 border-gray-700">
<CardContent className="p-4">
<div className="flex justify-between items-start">
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="font-semibold text-white">{tx.description}</span>
<Badge variant={tx.status === 'executed' ? 'default' : tx.status === 'pending' ? 'secondary' : 'destructive'}>
{tx.status}
</Badge>
</div>
<div className="text-sm text-gray-400">
To: {tx.to} | Amount: {tx.amount} {tx.token}
</div>
<div className="flex items-center gap-4">
<Progress value={(tx.currentSignatures / tx.requiredSignatures) * 100} className="w-32" />
<span className="text-sm text-gray-400">
{tx.currentSignatures}/{tx.requiredSignatures} signatures
</span>
</div>
</div>
{tx.status === 'pending' && (
<Button size="sm" onClick={() => handleSign(tx.id)} className="bg-blue-600 hover:bg-blue-700">
<Key className="w-4 h-4 mr-1" />
Sign
</Button>
)}
</div>
</CardContent>
</Card>
))}
</CardContent>
</Card>
</div>
);
};
+197
View File
@@ -0,0 +1,197 @@
import React, { useState } from 'react';
import { Send, Loader2, CheckCircle, XCircle } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { useWallet } from '@/contexts/WalletContext';
import { Alert, AlertDescription } from '@/components/ui/alert';
interface TransactionModalProps {
isOpen: boolean;
onClose: () => void;
type: 'send' | 'vote' | 'delegate';
data?: any;
}
export const TransactionModal: React.FC<TransactionModalProps> = ({
isOpen,
onClose,
type,
data
}) => {
const { address, signTransaction, signMessage } = useWallet();
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const [message, setMessage] = useState('');
const [loading, setLoading] = useState(false);
const [txHash, setTxHash] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const handleSendTransaction = async () => {
if (!recipient || !amount) {
setError('Please fill in all fields');
return;
}
setLoading(true);
setError(null);
try {
const tx = {
to: recipient,
value: '0x' + (parseFloat(amount) * 1e18).toString(16),
data: '0x',
};
const hash = await signTransaction(tx);
setTxHash(hash);
} catch (err: any) {
setError(err.message || 'Transaction failed');
} finally {
setLoading(false);
}
};
const handleSignMessage = async () => {
if (!message) {
setError('Please enter a message to sign');
return;
}
setLoading(true);
setError(null);
try {
const signature = await signMessage(message);
setTxHash(signature);
} catch (err: any) {
setError(err.message || 'Failed to sign message');
} finally {
setLoading(false);
}
};
const resetForm = () => {
setRecipient('');
setAmount('');
setMessage('');
setTxHash(null);
setError(null);
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={resetForm}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Send className="h-5 w-5 text-kesk" />
{type === 'send' ? 'Send PZK' : type === 'vote' ? 'Cast Vote' : 'Delegate Voting Power'}
</DialogTitle>
<DialogDescription>
{type === 'send'
? 'Send PZK tokens to another address'
: type === 'vote'
? 'Submit your vote for the proposal'
: 'Delegate your voting power to another address'}
</DialogDescription>
</DialogHeader>
{!txHash ? (
<div className="space-y-4">
{type === 'send' && (
<>
<div>
<Label htmlFor="recipient">Recipient Address</Label>
<Input
id="recipient"
placeholder="0x..."
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
className="font-mono"
/>
</div>
<div>
<Label htmlFor="amount">Amount (PZK)</Label>
<Input
id="amount"
type="number"
placeholder="0.0"
value={amount}
onChange={(e) => setAmount(e.target.value)}
/>
</div>
</>
)}
{type === 'vote' && (
<div>
<Label htmlFor="message">Vote Message</Label>
<Textarea
id="message"
placeholder="Enter your vote reason (optional)"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
)}
{error && (
<Alert variant="destructive">
<XCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="flex gap-2">
<Button
onClick={type === 'send' ? handleSendTransaction : handleSignMessage}
disabled={loading}
className="flex-1 bg-kesk hover:bg-kesk/90"
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
<>
<Send className="mr-2 h-4 w-4" />
{type === 'send' ? 'Send Transaction' : 'Sign & Submit'}
</>
)}
</Button>
<Button variant="outline" onClick={resetForm} disabled={loading}>
Cancel
</Button>
</div>
</div>
) : (
<div className="space-y-4">
<Alert className="border-kesk/20">
<CheckCircle className="h-4 w-4 text-kesk" />
<AlertDescription>
Transaction submitted successfully!
</AlertDescription>
</Alert>
<div className="p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground">Transaction Hash</div>
<div className="font-mono text-xs break-all">{txHash}</div>
</div>
<Button onClick={resetForm} className="w-full">
Close
</Button>
</div>
)}
</DialogContent>
</Dialog>
);
};
+101
View File
@@ -0,0 +1,101 @@
import React from 'react';
import { Wallet, LogOut, AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useWallet } from '@/contexts/WalletContext';
import { formatAddress, formatBalance } from '@/lib/wallet';
import { Badge } from '@/components/ui/badge';
export const WalletButton: React.FC = () => {
const {
isConnected,
address,
balance,
chainId,
error,
connectMetaMask,
disconnect,
switchNetwork
} = useWallet();
if (!isConnected) {
return (
<div className="flex items-center gap-2">
{error && (
<div className="flex items-center gap-2 text-sor text-sm">
<AlertCircle className="h-4 w-4" />
<span>{error}</span>
</div>
)}
<Button
onClick={connectMetaMask}
className="bg-kesk hover:bg-kesk/90 text-white"
>
<Wallet className="mr-2 h-4 w-4" />
Connect Wallet
</Button>
</div>
);
}
const isCorrectNetwork = chainId === '0x2329';
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="border-kesk/20 hover:border-kesk">
<div className="flex items-center gap-2">
<Wallet className="h-4 w-4 text-kesk" />
<div className="text-left">
<div className="text-sm font-medium">{formatAddress(address!)}</div>
<div className="text-xs text-muted-foreground">{formatBalance(balance)} PZK</div>
</div>
{!isCorrectNetwork && (
<Badge variant="destructive" className="ml-2 bg-sor">
Wrong Network
</Badge>
)}
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>Wallet Details</DropdownMenuLabel>
<DropdownMenuSeparator />
<div className="px-2 py-1.5">
<div className="text-sm text-muted-foreground">Address</div>
<div className="text-sm font-mono">{formatAddress(address!)}</div>
</div>
<div className="px-2 py-1.5">
<div className="text-sm text-muted-foreground">Balance</div>
<div className="text-sm font-medium">{formatBalance(balance)} PZK</div>
</div>
<div className="px-2 py-1.5">
<div className="text-sm text-muted-foreground">Network</div>
<div className="text-sm font-medium">
{isCorrectNetwork ? 'PezkuwiChain' : 'Unknown Network'}
</div>
</div>
<DropdownMenuSeparator />
{!isCorrectNetwork && (
<>
<DropdownMenuItem onClick={switchNetwork} className="text-zer">
Switch to PezkuwiChain
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem onClick={disconnect} className="text-sor">
<LogOut className="mr-2 h-4 w-4" />
Disconnect
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
+127
View File
@@ -0,0 +1,127 @@
import React, { useState } from 'react';
import { Wallet, Chrome, Smartphone, Copy, Check, ExternalLink } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useWallet } from '@/contexts/WalletContext';
import { formatAddress } from '@/lib/wallet';
interface WalletModalProps {
isOpen: boolean;
onClose: () => void;
}
export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) => {
const { connectMetaMask, connectWalletConnect, isConnected, address } = useWallet();
const [copied, setCopied] = useState(false);
const handleCopyAddress = () => {
if (address) {
navigator.clipboard.writeText(address);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleMetaMaskConnect = async () => {
await connectMetaMask();
if (isConnected) onClose();
};
const handleWalletConnectConnect = async () => {
await connectWalletConnect();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Wallet className="h-5 w-5 text-kesk" />
Connect Wallet
</DialogTitle>
<DialogDescription>
Connect your wallet to interact with PezkuwiChain governance
</DialogDescription>
</DialogHeader>
{!isConnected ? (
<Tabs defaultValue="browser" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="browser">Browser Wallet</TabsTrigger>
<TabsTrigger value="mobile">Mobile Wallet</TabsTrigger>
</TabsList>
<TabsContent value="browser" className="space-y-4">
<Button
onClick={handleMetaMaskConnect}
className="w-full justify-start bg-kesk hover:bg-kesk/90"
>
<Chrome className="mr-2 h-5 w-5" />
MetaMask
</Button>
<div className="text-sm text-muted-foreground">
Don't have MetaMask?{' '}
<a
href="https://metamask.io/download/"
target="_blank"
rel="noopener noreferrer"
className="text-kesk hover:underline"
>
Download here
</a>
</div>
</TabsContent>
<TabsContent value="mobile" className="space-y-4">
<Button
onClick={handleWalletConnectConnect}
className="w-full justify-start bg-zer hover:bg-zer/90"
>
<Smartphone className="mr-2 h-5 w-5" />
WalletConnect
</Button>
<div className="text-sm text-muted-foreground">
Scan QR code with your mobile wallet to connect
</div>
</TabsContent>
</Tabs>
) : (
<div className="space-y-4">
<div className="flex items-center justify-between p-3 border rounded-lg">
<div>
<div className="text-sm text-muted-foreground">Connected Address</div>
<div className="font-mono font-medium">{formatAddress(address!)}</div>
</div>
<Button
size="icon"
variant="ghost"
onClick={handleCopyAddress}
>
{copied ? (
<Check className="h-4 w-4 text-kesk" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
<Button
variant="outline"
className="w-full"
onClick={() => window.open('https://explorer.pezkuwichain.app/address/' + address, '_blank')}
>
<ExternalLink className="mr-2 h-4 w-4" />
View on Explorer
</Button>
</div>
)}
</DialogContent>
</Dialog>
);
};

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