diff --git a/src/App.tsx b/src/App.tsx index ba8a9bca..f2487a0a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import Lenis from 'lenis'; import { useEffect } from 'react'; import { Toaster } from 'react-hot-toast'; import { createBrowserRouter, RouterProvider } from 'react-router'; + import AppErrorBoundary from './components/common/AppErrorBoundary'; import OfflineBanner from './components/common/OfflineBanner'; import SessionExpiryWatcher from './components/common/SessionExpiryWatcher'; diff --git a/src/components/common/CreatorKeyDashboard.tsx b/src/components/common/CreatorKeyDashboard.tsx new file mode 100644 index 00000000..995ac7f8 --- /dev/null +++ b/src/components/common/CreatorKeyDashboard.tsx @@ -0,0 +1,745 @@ +import React, { useState, useEffect } from 'react'; +import { useAccount } from 'wagmi'; +import { + useCreatorKeyStore, + selectSelectedKey, +} from '@/hooks/useCreatorKeyStore'; +import { + validateKeyConfig, + validateKeyMetadata, + CONFIG_BOUNDS, + type KeyConfig, + type KeyMetadata, +} from '@/services/creatorKey.service'; +import { + Key, + Users, + BarChart3, + TrendingUp, + Sliders, + Edit3, + Clock, + CheckCircle, + AlertCircle, + Shield, + ExternalLink, + RefreshCw, +} from 'lucide-react'; + +export interface CreatorKeyDashboardProps { + creatorAddress?: string; + className?: string; +} + +export const CreatorKeyDashboard: React.FC = ({ + creatorAddress, + className = '', +}) => { + const { address, isConnected } = useAccount(); + const effectiveAddress = creatorAddress ?? address; + const effectiveConnected = Boolean(creatorAddress || (isConnected && address)); + const { + keys, + selectedKeyId, + isLoading, + isSubmitting, + txStatus, + txMessage, + timelockNotice, + fetchKeys, + selectKey, + updateMetadata, + updateConfig, + clearStatus, + } = useCreatorKeyStore(); + + const selectedKey = selectSelectedKey({ keys, selectedKeyId }); + + const [activeTab, setActiveTab] = useState<'analytics' | 'metadata' | 'config'>('analytics'); + + // Metadata form state + const [metaForm, setMetaForm] = useState({ + name: '', + symbol: '', + description: '', + imageUri: '', + }); + const [metaErrors, setMetaErrors] = useState>>({}); + + // Config form state + const [configForm, setConfigForm] = useState({ + spreadBps: 500, + cooldownSeconds: 300, + holdingCap: 50, + }); + const [configErrors, setConfigErrors] = useState>>({}); + + // Fetch keys on connect or address change + useEffect(() => { + if (effectiveConnected && effectiveAddress) { + void fetchKeys(effectiveAddress); + } else { + void fetchKeys(undefined); + } + }, [effectiveConnected, effectiveAddress, fetchKeys]); + + // Sync local forms when selected key changes + useEffect(() => { + if (selectedKey) { + setMetaForm({ + name: selectedKey.metadata.name, + symbol: selectedKey.metadata.symbol, + description: selectedKey.metadata.description, + imageUri: selectedKey.metadata.imageUri, + }); + setMetaErrors({}); + + setConfigForm({ + spreadBps: selectedKey.config.spreadBps, + cooldownSeconds: selectedKey.config.cooldownSeconds, + holdingCap: selectedKey.config.holdingCap, + }); + setConfigErrors({}); + } + }, [selectedKey]); + + // Metadata submission handler + const handleSaveMetadata = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedKey) return; + + const validation = validateKeyMetadata(metaForm); + if (!validation.isValid) { + setMetaErrors(validation.errors); + return; + } + setMetaErrors({}); + + await updateMetadata(selectedKey.id, metaForm); + }; + + // Config submission handler + const handleSaveConfig = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedKey || !effectiveAddress) return; + + const validation = validateKeyConfig(configForm); + if (!validation.isValid) { + setConfigErrors(validation.errors); + return; + } + setConfigErrors({}); + + await updateConfig(selectedKey.id, configForm, effectiveAddress); + }; + + // Helper for format cooldown seconds into readable string + const formatDuration = (secs: number) => { + if (secs < 60) return `${secs} seconds`; + if (secs < 3600) return `${Math.round(secs / 60)} minutes`; + if (secs < 86400) return `${Math.round(secs / 3600)} hours`; + return `${(secs / 86400).toFixed(1)} days`; + }; + + return ( +
+ {/* Top Header */} +
+
+
+ +
+
+

+ Creator Key Management +

+

+ Manage your deployed creator keys, modify on-chain configurations, and track per-key analytics. +

+
+
+ + {/* Connected Wallet Badge */} +
+ {effectiveConnected && effectiveAddress ? ( +
+ + Creator: {effectiveAddress.slice(0, 6)}...{effectiveAddress.slice(-4)} + ({keys.length} {keys.length === 1 ? 'key' : 'keys'}) +
+ ) : ( +
+ + Wallet not connected +
+ )} + + {effectiveConnected && effectiveAddress && ( + + )} +
+
+ + {/* Non-connected / Empty States */} + {!effectiveConnected ? ( +
+ +

Connect Creator Wallet

+

+ Connect your wallet to access your deployed keys, update bond parameters, edit metadata, and view real-time trading metrics. +

+
+ ) : keys.length === 0 && !isLoading ? ( +
+ +

No Deployed Keys Found

+

+ Only creator keys deployed by {effectiveAddress} are displayed here. Deploy a key through the creator launcher to start managing it. +

+
+ ) : ( + <> + {/* Deployed Keys Selector */} +
+ +
+ {keys.map(k => { + const isSelected = k.id === selectedKeyId; + return ( + + ); + })} +
+
+ + {selectedKey && ( + <> + {/* Per-Key Analytics Overview Cards */} +
+ {/* Holders */} +
+
+ + Key Holders +
+
+ {selectedKey.analytics.holderCount.toLocaleString()} +
+
Active unique owners
+
+ + {/* Volume */} +
+
+ + Trade Volume +
+
+ {selectedKey.analytics.volumeXlm.toLocaleString()} XLM +
+
+ ≈ ${selectedKey.analytics.volumeUsd.toLocaleString()} USD +
+
+ + {/* Trades */} +
+
+ + Total Trades +
+
+ {selectedKey.analytics.tradeCount.toLocaleString()} +
+
Buys & sells combined
+
+ + {/* Current Price */} +
+
+ + Current Price +
+
+ {selectedKey.analytics.currentPriceXlm} XLM +
+
+ ≈ ${selectedKey.analytics.currentPriceUsd} USD +
+
+
+ + {/* Feedback & Timelock Banners */} + {txStatus !== 'idle' && txMessage && ( +
+ {txStatus === 'success' ? ( + + ) : txStatus === 'error' ? ( + + ) : ( + + )} +
{txMessage}
+ +
+ )} + + {/* Timelock Notice Alert */} + {timelockNotice && ( +
+ +
+

+ Timelock Notice ({timelockNotice.delayHours}h Execution Delay Required) +

+

+ {timelockNotice.message} The proposed change has been added to the pending timelock governance queue and can be executed once the buffer elapses. +

+
+
+ )} + + {/* Navigation Sub-Tabs */} +
+ + + +
+ + {/* TAB 1: OVERVIEW */} + {activeTab === 'analytics' && ( +
+
+

Key Details & Parameters

+
+
+ Contract Address +
+ {selectedKey.contractAddress} + +
+
+
+ Deployment Date + + {new Date(selectedKey.deployedAt).toLocaleDateString()} + +
+
+ Current Trading Spread + + {selectedKey.config.spreadBps} bps ({(selectedKey.config.spreadBps / 100).toFixed(1)}%) + +
+
+ Anti-Frontrun Cooldown + + {formatDuration(selectedKey.config.cooldownSeconds)} ({selectedKey.config.cooldownSeconds}s) + +
+
+ Max Holding Cap + + {selectedKey.config.holdingCap.toLocaleString()} keys per wallet + +
+
+ Description +

+ {selectedKey.metadata.description || 'No description provided.'} +

+
+
+
+
+ )} + + {/* TAB 2: EDIT METADATA FORM */} + {activeTab === 'metadata' && ( +
+
+ {/* Key Name */} +
+ + setMetaForm(prev => ({ ...prev, name: e.target.value }))} + className="w-full bg-slate-950/60 border border-white/10 rounded-xl px-3.5 py-2.5 text-sm text-white focus:outline-none focus:border-violet-500 transition-colors" + placeholder="e.g. Creator Access Key" + disabled={isSubmitting} + /> + {metaErrors.name && ( + + {metaErrors.name} + + )} +
+ + {/* Symbol */} +
+ + setMetaForm(prev => ({ ...prev, symbol: e.target.value.toUpperCase() }))} + className="w-full bg-slate-950/60 border border-white/10 rounded-xl px-3.5 py-2.5 text-sm text-white font-mono focus:outline-none focus:border-violet-500 transition-colors" + placeholder="e.g. RIVER" + disabled={isSubmitting} + /> + {metaErrors.symbol && ( + + {metaErrors.symbol} + + )} +
+
+ + {/* Image URI with preview */} +
+ +
+ setMetaForm(prev => ({ ...prev, imageUri: e.target.value }))} + className="flex-1 bg-slate-950/60 border border-white/10 rounded-xl px-3.5 py-2.5 text-sm text-white font-mono text-xs focus:outline-none focus:border-violet-500 transition-colors" + placeholder="https://... or ipfs://..." + disabled={isSubmitting} + /> + {metaForm.imageUri && ( + Preview { + // Hide broken image preview + (e.target as HTMLElement).style.display = 'none'; + }} + /> + )} +
+ {metaErrors.imageUri && ( + + {metaErrors.imageUri} + + )} +
+ + {/* Description */} +
+ +