From bc7ab9b2a41de63ff0829efb2cdbe2501b166a5b Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Sat, 31 Jan 2026 08:51:45 +0300 Subject: [PATCH] Initial P2P mobile frontend - Copy P2P components from pwap/web - Mobile-optimized P2P trading interface - To be deployed at telegram.pezkuwichain.io/p2p --- .gitignore | 24 + README.md | 73 ++ eslint.config.js | 23 + index.html | 13 + package.json | 51 ++ postcss.config.js | 6 + public/vite.svg | 1 + src/App.tsx | 20 + src/components/p2p/AdList.tsx | 311 +++++++++ src/components/p2p/BlockTrade.tsx | 385 +++++++++++ src/components/p2p/CreateAd.tsx | 385 +++++++++++ src/components/p2p/DepositModal.tsx | 450 +++++++++++++ src/components/p2p/DisputeModal.tsx | 420 ++++++++++++ src/components/p2p/ExpressMode.tsx | 369 +++++++++++ src/components/p2p/InternalBalanceCard.tsx | 176 +++++ src/components/p2p/MerchantApplication.tsx | 506 ++++++++++++++ src/components/p2p/MerchantTierBadge.tsx | 110 +++ src/components/p2p/NotificationBell.tsx | 284 ++++++++ src/components/p2p/OrderFilters.tsx | 490 ++++++++++++++ src/components/p2p/P2PDashboard.tsx | 295 ++++++++ src/components/p2p/RatingModal.tsx | 278 ++++++++ src/components/p2p/TradeChat.tsx | 415 ++++++++++++ src/components/p2p/TradeModal.tsx | 204 ++++++ src/components/p2p/WithdrawModal.tsx | 458 +++++++++++++ src/components/p2p/types.ts | 46 ++ src/components/ui/accordion.tsx | 56 ++ src/components/ui/alert-dialog.tsx | 139 ++++ src/components/ui/alert.tsx | 65 ++ src/components/ui/aspect-ratio.tsx | 5 + src/components/ui/avatar.tsx | 54 ++ src/components/ui/badge.test.tsx | 13 + src/components/ui/badge.tsx | 48 ++ src/components/ui/breadcrumb.tsx | 115 ++++ src/components/ui/button.tsx | 56 ++ src/components/ui/calendar.tsx | 64 ++ src/components/ui/card.tsx | 79 +++ src/components/ui/carousel.tsx | 260 ++++++++ src/components/ui/chart.tsx | 363 ++++++++++ src/components/ui/checkbox.tsx | 28 + src/components/ui/collapsible.tsx | 9 + src/components/ui/command.tsx | 151 +++++ src/components/ui/context-menu.tsx | 198 ++++++ src/components/ui/dialog.tsx | 120 ++++ src/components/ui/drawer.tsx | 116 ++++ src/components/ui/dropdown-menu.tsx | 198 ++++++ src/components/ui/form.tsx | 176 +++++ src/components/ui/hover-card.tsx | 27 + src/components/ui/input-otp.tsx | 69 ++ src/components/ui/input.tsx | 24 + src/components/ui/label.tsx | 43 ++ src/components/ui/menubar.tsx | 234 +++++++ src/components/ui/navigation-menu.tsx | 128 ++++ src/components/ui/pagination.tsx | 119 ++++ src/components/ui/popover.tsx | 29 + src/components/ui/progress.tsx | 37 ++ src/components/ui/radio-group.tsx | 42 ++ src/components/ui/resizable.tsx | 43 ++ src/components/ui/scroll-area.tsx | 48 ++ src/components/ui/select.tsx | 158 +++++ src/components/ui/separator.tsx | 37 ++ src/components/ui/sheet.tsx | 131 ++++ src/components/ui/sidebar.tsx | 738 +++++++++++++++++++++ src/components/ui/skeleton.tsx | 25 + src/components/ui/slider.tsx | 26 + src/components/ui/sonner.tsx | 31 + src/components/ui/switch.tsx | 27 + src/components/ui/table.tsx | 117 ++++ src/components/ui/tabs.tsx | 53 ++ src/components/ui/textarea.tsx | 22 + src/components/ui/toast.tsx | 127 ++++ src/components/ui/toaster.tsx | 33 + src/components/ui/toggle-group.tsx | 67 ++ src/components/ui/toggle.tsx | 45 ++ src/components/ui/tooltip.tsx | 28 + src/components/ui/use-toast.ts | 3 + src/contexts/AppContext.tsx | 36 + src/contexts/AuthContext.tsx | 309 +++++++++ src/contexts/DashboardContext.tsx | 98 +++ src/contexts/IdentityContext.tsx | 162 +++++ src/contexts/PezkuwiContext.tsx | 330 +++++++++ src/contexts/ReferralContext.tsx | 177 +++++ src/contexts/WalletContext.tsx | 327 +++++++++ src/contexts/WebSocketContext.tsx | 172 +++++ src/index.css | 123 ++++ src/lib/mobile-bridge.ts | 195 ++++++ src/lib/sentry.ts | 91 +++ src/lib/supabase.ts | 13 + src/lib/utils.test.ts | 20 + src/lib/utils.ts | 14 + src/main.tsx | 9 + tailwind.config.ts | 132 ++++ tsconfig.app.json | 28 + tsconfig.json | 7 + tsconfig.node.json | 26 + vite.config.ts | 7 + 95 files changed, 12893 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/vite.svg create mode 100644 src/App.tsx create mode 100644 src/components/p2p/AdList.tsx create mode 100644 src/components/p2p/BlockTrade.tsx create mode 100644 src/components/p2p/CreateAd.tsx create mode 100644 src/components/p2p/DepositModal.tsx create mode 100644 src/components/p2p/DisputeModal.tsx create mode 100644 src/components/p2p/ExpressMode.tsx create mode 100644 src/components/p2p/InternalBalanceCard.tsx create mode 100644 src/components/p2p/MerchantApplication.tsx create mode 100644 src/components/p2p/MerchantTierBadge.tsx create mode 100644 src/components/p2p/NotificationBell.tsx create mode 100644 src/components/p2p/OrderFilters.tsx create mode 100644 src/components/p2p/P2PDashboard.tsx create mode 100644 src/components/p2p/RatingModal.tsx create mode 100644 src/components/p2p/TradeChat.tsx create mode 100644 src/components/p2p/TradeModal.tsx create mode 100644 src/components/p2p/WithdrawModal.tsx create mode 100644 src/components/p2p/types.ts create mode 100644 src/components/ui/accordion.tsx create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/components/ui/alert.tsx create mode 100644 src/components/ui/aspect-ratio.tsx create mode 100644 src/components/ui/avatar.tsx create mode 100644 src/components/ui/badge.test.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/breadcrumb.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/calendar.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/carousel.tsx create mode 100644 src/components/ui/chart.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/collapsible.tsx create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/context-menu.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/drawer.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/hover-card.tsx create mode 100644 src/components/ui/input-otp.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/menubar.tsx create mode 100644 src/components/ui/navigation-menu.tsx create mode 100644 src/components/ui/pagination.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/components/ui/radio-group.tsx create mode 100644 src/components/ui/resizable.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/components/ui/sheet.tsx create mode 100644 src/components/ui/sidebar.tsx create mode 100644 src/components/ui/skeleton.tsx create mode 100644 src/components/ui/slider.tsx create mode 100644 src/components/ui/sonner.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/table.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/components/ui/toast.tsx create mode 100644 src/components/ui/toaster.tsx create mode 100644 src/components/ui/toggle-group.tsx create mode 100644 src/components/ui/toggle.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/components/ui/use-toast.ts create mode 100644 src/contexts/AppContext.tsx create mode 100644 src/contexts/AuthContext.tsx create mode 100644 src/contexts/DashboardContext.tsx create mode 100644 src/contexts/IdentityContext.tsx create mode 100644 src/contexts/PezkuwiContext.tsx create mode 100644 src/contexts/ReferralContext.tsx create mode 100644 src/contexts/WalletContext.tsx create mode 100644 src/contexts/WebSocketContext.tsx create mode 100644 src/index.css create mode 100644 src/lib/mobile-bridge.ts create mode 100644 src/lib/sentry.ts create mode 100644 src/lib/supabase.ts create mode 100644 src/lib/utils.test.ts create mode 100644 src/lib/utils.ts create mode 100644 src/main.tsx create mode 100644 tailwind.config.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -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? diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +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' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..0e1285c --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + pezkuwi-p2p-mobile + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..005f1a0 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "pezkuwi-p2p-mobile", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint ." + }, + "dependencies": { + "@pezkuwi/api": "^16.5.18", + "@pezkuwi/keyring": "^14.0.13", + "@pezkuwi/util": "^14.0.13", + "@pezkuwi/util-crypto": "^14.0.13", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.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-tooltip": "^1.1.4", + "@supabase/supabase-js": "^2.49.4", + "@tanstack/react-query": "^5.56.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "lucide-react": "^0.462.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "sonner": "^1.5.0", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.11", + "typescript": "^5.5.3", + "vite": "^7.3.1" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..b21ef4e --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,20 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { P2PDashboard } from './components/p2p/P2PDashboard'; +import { AuthProvider } from './contexts/AuthContext'; +import './index.css'; + +const queryClient = new QueryClient(); + +function App() { + return ( + + +
+ +
+
+
+ ); +} + +export default App; diff --git a/src/components/p2p/AdList.tsx b/src/components/p2p/AdList.tsx new file mode 100644 index 0000000..249f852 --- /dev/null +++ b/src/components/p2p/AdList.tsx @@ -0,0 +1,311 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { Loader2, Shield, Zap } from 'lucide-react'; +import { useAuth } from '@/contexts/AuthContext'; +import { TradeModal } from './TradeModal'; +import { MerchantTierBadge } from './MerchantTierBadge'; +import { getUserReputation, type P2PFiatOffer, type P2PReputation } from '@shared/lib/p2p-fiat'; +import { supabase } from '@/lib/supabase'; +import type { P2PFilters } from './types'; + +interface AdListProps { + type: 'buy' | 'sell' | 'my-ads'; + filters?: P2PFilters; +} + +interface OfferWithReputation extends P2PFiatOffer { + seller_reputation?: P2PReputation; + payment_method_name?: string; + merchant_tier?: 'lite' | 'super' | 'diamond'; +} + +export function AdList({ type, filters }: AdListProps) { + const { user } = useAuth(); + const [offers, setOffers] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedOffer, setSelectedOffer] = useState(null); + + useEffect(() => { + fetchOffers(); + + // Refresh data when user returns to the tab (visibility change) + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + fetchOffers(); + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [type, user, filters]); + + const fetchOffers = async () => { + setLoading(true); + try { + let offersData: P2PFiatOffer[] = []; + + // Build base query + let query = supabase.from('p2p_fiat_offers').select('*'); + + if (type === 'buy') { + // Buy tab = show SELL offers (user wants to buy from sellers) + query = query.eq('ad_type', 'sell').eq('status', 'open').gt('remaining_amount', 0); + } else if (type === 'sell') { + // Sell tab = show BUY offers (user wants to sell to buyers) + query = query.eq('ad_type', 'buy').eq('status', 'open').gt('remaining_amount', 0); + } else if (type === 'my-ads' && user) { + // My offers - show all of user's offers + query = query.eq('seller_id', user.id); + } + + // Apply filters if provided + if (filters) { + // Token filter + if (filters.token && filters.token !== 'all') { + query = query.eq('token', filters.token); + } + + // Fiat currency filter + if (filters.fiatCurrency && filters.fiatCurrency !== 'all') { + query = query.eq('fiat_currency', filters.fiatCurrency); + } + + // Payment method filter + if (filters.paymentMethods && filters.paymentMethods.length > 0) { + query = query.in('payment_method_id', filters.paymentMethods); + } + + // Amount range filter + if (filters.minAmount !== null) { + query = query.gte('remaining_amount', filters.minAmount); + } + if (filters.maxAmount !== null) { + query = query.lte('remaining_amount', filters.maxAmount); + } + + // Sort order + const sortColumn = filters.sortBy === 'price' ? 'price_per_unit' : + filters.sortBy === 'completion_rate' ? 'created_at' : + filters.sortBy === 'trades' ? 'created_at' : + 'created_at'; + query = query.order(sortColumn, { ascending: filters.sortOrder === 'asc' }); + } else { + query = query.order('created_at', { ascending: false }); + } + + const { data } = await query; + offersData = data || []; + + // Enrich with reputation, payment method, and merchant tier + const enrichedOffers = await Promise.all( + offersData.map(async (offer) => { + const [reputation, paymentMethod, merchantTier] = await Promise.all([ + getUserReputation(offer.seller_id), + supabase + .from('payment_methods') + .select('method_name') + .eq('id', offer.payment_method_id) + .single(), + supabase + .from('p2p_merchant_tiers') + .select('tier') + .eq('user_id', offer.seller_id) + .single() + ]); + + return { + ...offer, + seller_reputation: reputation || undefined, + payment_method_name: paymentMethod.data?.method_name, + merchant_tier: merchantTier.data?.tier as 'lite' | 'super' | 'diamond' | undefined + }; + }) + ); + + // Apply client-side filters (completion rate, merchant tier) + let filteredOffers = enrichedOffers; + + if (filters) { + // Completion rate filter (needs reputation data) + if (filters.minCompletionRate > 0) { + filteredOffers = filteredOffers.filter(offer => { + if (!offer.seller_reputation) return false; + const rate = (offer.seller_reputation.completed_trades / (offer.seller_reputation.total_trades || 1)) * 100; + return rate >= filters.minCompletionRate; + }); + } + + // Merchant tier filter + if (filters.merchantTiers && filters.merchantTiers.length > 0) { + filteredOffers = filteredOffers.filter(offer => { + if (!offer.merchant_tier) return false; + // If super is selected, include super and diamond + // If diamond is selected, include only diamond + if (filters.merchantTiers.includes('diamond')) { + return offer.merchant_tier === 'diamond'; + } + if (filters.merchantTiers.includes('super')) { + return offer.merchant_tier === 'super' || offer.merchant_tier === 'diamond'; + } + return filters.merchantTiers.includes(offer.merchant_tier); + }); + } + + // Verified only filter + if (filters.verifiedOnly) { + filteredOffers = filteredOffers.filter(offer => offer.seller_reputation?.verified_merchant); + } + } + + setOffers(filteredOffers); + } catch (error) { + if (import.meta.env.DEV) console.error('Fetch offers error:', error); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (offers.length === 0) { + return ( +
+

+ {type === 'my-ads' ? 'You have no active offers' : 'No offers available'} +

+
+ ); + } + + return ( +
+ {offers.map(offer => ( + + +
+ {/* Seller Info */} +
+ + + {offer.seller_wallet.slice(0, 2).toUpperCase()} + + +
+
+

+ {offer.seller_wallet.slice(0, 6)}...{offer.seller_wallet.slice(-4)} +

+ {offer.merchant_tier && ( + + )} + {offer.seller_reputation?.verified_merchant && ( + + )} + {offer.seller_reputation?.fast_trader && ( + + )} +
+ {offer.seller_reputation && ( +

+ {offer.seller_reputation.completed_trades} trades • {' '} + {((offer.seller_reputation.completed_trades / (offer.seller_reputation.total_trades || 1)) * 100).toFixed(0)}% completion +

+ )} +
+
+ + {/* Price */} +
+

Price

+

+ {offer.price_per_unit.toFixed(2)} {offer.fiat_currency} +

+
+ + {/* Available */} +
+

Available

+

+ {offer.remaining_amount} {offer.token} +

+ {offer.min_order_amount && ( +

+ Min: {offer.min_order_amount} {offer.token} +

+ )} +
+ + {/* Payment Method */} +
+

Payment

+ + {offer.payment_method_name || 'N/A'} + +

+ {offer.time_limit_minutes} min limit +

+
+ + {/* Action */} +
+ {offer.seller_id === user?.id && type !== 'my-ads' && ( + + Your Ad + + )} + +
+
+ + {/* Status badge for my-ads */} + {type === 'my-ads' && ( +
+
+ + {offer.status.toUpperCase()} + +

+ Created: {new Date(offer.created_at).toLocaleDateString()} +

+
+
+ )} +
+
+ ))} + + {selectedOffer && ( + { + setSelectedOffer(null); + fetchOffers(); // Refresh list + + }} + /> + )} +
+ ); +} diff --git a/src/components/p2p/BlockTrade.tsx b/src/components/p2p/BlockTrade.tsx new file mode 100644 index 0000000..d2399ca --- /dev/null +++ b/src/components/p2p/BlockTrade.tsx @@ -0,0 +1,385 @@ +/** + * Block Trade Component - OKX-Style OTC Trading + * + * Block trades are for large volume trades that are handled differently + * from regular P2P trades. They offer: + * - Custom pricing negotiation + * - Dedicated support + * - Multi-tranche settlements + * - Enhanced privacy + */ + +import React, { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } 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 { Textarea } from '@/components/ui/textarea'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + Blocks, Shield, Clock, Lock, MessageSquare, ChevronRight, + Building2, AlertTriangle +} from 'lucide-react'; +import { supabase } from '@/lib/supabase'; +import { useAuth } from '@/contexts/AuthContext'; +import { toast } from 'sonner'; +import type { CryptoToken, FiatCurrency } from '@pezkuwi/lib/p2p-fiat'; + +interface BlockTradeRequest { + id: string; + type: 'buy' | 'sell'; + token: CryptoToken; + fiat_currency: FiatCurrency; + amount: number; + target_price?: number; + message?: string; + status: 'pending' | 'negotiating' | 'approved' | 'in_progress' | 'completed' | 'cancelled'; + created_at: string; +} + +const SUPPORTED_TOKENS: CryptoToken[] = ['HEZ', 'PEZ']; + +// All supported fiat currencies including Kurdish Diaspora countries +const SUPPORTED_FIATS: { code: FiatCurrency; name: string; symbol: string; region: string }[] = [ + // Primary regions (Kurdistan & neighboring) + { code: 'TRY', name: 'Turkish Lira', symbol: '₺', region: 'Bakur' }, + { code: 'IQD', name: 'Iraqi Dinar', symbol: 'د.ع', region: 'Başûr' }, + { code: 'IRR', name: 'Iranian Rial', symbol: '﷼', region: 'Rojhilat' }, + // Eurozone diaspora + { code: 'EUR', name: 'Euro', symbol: '€', region: 'EU' }, + // Other diaspora regions + { code: 'USD', name: 'US Dollar', symbol: '$', region: 'USA' }, + { code: 'GBP', name: 'British Pound', symbol: '£', region: 'UK' }, + { code: 'SEK', name: 'Swedish Krona', symbol: 'kr', region: 'Sweden' }, + { code: 'CHF', name: 'Swiss Franc', symbol: 'Fr.', region: 'Switzerland' }, + { code: 'NOK', name: 'Norwegian Krone', symbol: 'kr', region: 'Norway' }, + { code: 'DKK', name: 'Danish Krone', symbol: 'kr', region: 'Denmark' }, + { code: 'AUD', name: 'Australian Dollar', symbol: 'A$', region: 'Australia' }, + { code: 'CAD', name: 'Canadian Dollar', symbol: 'C$', region: 'Canada' }, +]; + +// Minimum amounts for block trade (in USD equivalent) +const MINIMUM_BLOCK_AMOUNTS: Record = { + HEZ: 10000, // 10,000 HEZ minimum + PEZ: 50000, // 50,000 PEZ minimum +}; + +export function BlockTrade() { + const [showRequestModal, setShowRequestModal] = useState(false); + const [type, setType] = useState<'buy' | 'sell'>('buy'); + const [token, setToken] = useState('HEZ'); + const [fiat, setFiat] = useState('USD'); + const [amount, setAmount] = useState(''); + const [targetPrice, setTargetPrice] = useState(''); + const [message, setMessage] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [requests, setRequests] = useState([]); + + const { user } = useAuth(); + const fiatSymbol = SUPPORTED_FIATS.find(f => f.code === fiat)?.symbol || ''; + const minAmount = MINIMUM_BLOCK_AMOUNTS[token]; + + // Fetch user's block trade requests + React.useEffect(() => { + if (!user) return; + + const fetchRequests = async () => { + const { data, error } = await supabase + .from('p2p_block_trade_requests') + .select('*') + .eq('user_id', user.id) + .order('created_at', { ascending: false }); + + if (!error && data) { + setRequests(data); + } + }; + + fetchRequests(); + }, [user]); + + const handleSubmitRequest = async () => { + if (!user) { + toast.error('Please login to submit a block trade request'); + return; + } + + const amountNum = parseFloat(amount); + if (isNaN(amountNum) || amountNum < minAmount) { + toast.error(`Minimum amount for ${token} block trade is ${minAmount.toLocaleString()} ${token}`); + return; + } + + setIsSubmitting(true); + try { + const { data, error } = await supabase + .from('p2p_block_trade_requests') + .insert({ + user_id: user.id, + type, + token, + fiat_currency: fiat, + amount: amountNum, + target_price: targetPrice ? parseFloat(targetPrice) : null, + message: message || null, + status: 'pending' + }) + .select() + .single(); + + if (error) throw error; + + toast.success('Block trade request submitted! Our OTC desk will contact you within 24 hours.'); + setShowRequestModal(false); + setAmount(''); + setTargetPrice(''); + setMessage(''); + + // Add to local state + setRequests(prev => [data, ...prev]); + } catch (err) { + console.error('Block trade request error:', err); + toast.error('Failed to submit request'); + } finally { + setIsSubmitting(false); + } + }; + + const getStatusBadge = (status: BlockTradeRequest['status']) => { + const styles: Record = { + pending: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30', + negotiating: 'bg-blue-500/20 text-blue-400 border-blue-500/30', + approved: 'bg-green-500/20 text-green-400 border-green-500/30', + in_progress: 'bg-purple-500/20 text-purple-400 border-purple-500/30', + completed: 'bg-gray-500/20 text-gray-400 border-gray-500/30', + cancelled: 'bg-red-500/20 text-red-400 border-red-500/30', + }; + return styles[status] || styles.pending; + }; + + return ( + <> + + +
+
+
+ +
+
+ Block Trade (OTC) + + Large volume trades with custom pricing + +
+
+ + VIP + +
+
+ + {/* Features */} +
+
+ + Private Negotiation +
+
+ + Escrow Protected +
+
+ + Dedicated Support +
+
+ + Flexible Settlement +
+
+ + {/* Minimum Amounts Info */} +
+

Minimum Block Trade Amounts:

+
+ {Object.entries(MINIMUM_BLOCK_AMOUNTS).map(([t, min]) => ( + + {min.toLocaleString()} {t} + + ))} +
+
+ + {/* Request Button */} + + + {/* Active Requests */} + {requests.length > 0 && ( +
+

Your Requests:

+ {requests.slice(0, 3).map(req => ( +
+
+ + {req.type.toUpperCase()} + + + {req.amount.toLocaleString()} {req.token} + +
+ + {req.status.replace('_', ' ')} + +
+ ))} +
+ )} +
+
+ + {/* Request Modal */} + + + + + + Block Trade Request + + + Submit a request for our OTC desk to handle your large volume trade. + + + +
+ {/* Buy/Sell Toggle */} + setType(v as 'buy' | 'sell')}> + + + Buy + + + Sell + + + + + {/* Token & Fiat */} +
+
+ + +
+
+ + +
+
+ + {/* Amount */} +
+ + setAmount(e.target.value)} + className="bg-gray-800 border-gray-700" + /> +

+ Minimum: {minAmount.toLocaleString()} {token} +

+
+ + {/* Target Price (Optional) */} +
+ +
+ setTargetPrice(e.target.value)} + className="bg-gray-800 border-gray-700 pr-16" + /> + + {fiatSymbol}/{token} + +
+
+ + {/* Message */} +
+ +