Download src/components/AdminPanel.tsx from SherlockHolmes786/tradewithlive: direct link, hf CLI and curl.
- Browser
- Download file 22.5 kB
-
https://huggingface.co/SherlockHolmes786/tradewithlive/resolve/main/src/components/AdminPanel.tsx
- Command line
-
hf download hf://SherlockHolmes786/tradewithlive/src/components/AdminPanel.tsx
-
curl -L -o AdminPanel.tsx https://huggingface.co/SherlockHolmes786/tradewithlive/resolve/main/src/components/AdminPanel.tsx
22.5 kB
| import { useState, useEffect } from 'react'; | |
| import { Settings, Link as LinkIcon, Trash2, RefreshCw, CheckCircle, XCircle, AlertCircle, UserPlus } from 'lucide-react'; | |
| import { cn } from '../lib/utils'; | |
| import { BrokerConnection } from '../types/broker'; | |
| import { InfluencerRegistration, ApprovalStatus } from '../types/influencerRegistration'; | |
| import { removeBrokerCredentials, clearAllBrokerCredentials } from '../config/brokerConfig'; | |
| import { influencers } from '../config/data'; | |
| interface AdminPanelProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| } | |
| export const AdminPanel = ({ isOpen, onClose }: AdminPanelProps) => { | |
| const [connections, setConnections] = useState<BrokerConnection[]>([]); | |
| const [registrations, setRegistrations] = useState<InfluencerRegistration[]>([]); | |
| const [activeTab, setActiveTab] = useState<'connections' | 'registrations'>('connections'); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| // Load connections when panel opens | |
| useEffect(() => { | |
| if (isOpen) { | |
| loadConnections(); | |
| loadRegistrations(); | |
| } | |
| }, [isOpen]); | |
| const loadConnections = () => { | |
| setIsLoading(true); | |
| setError(null); | |
| try { | |
| const brokerCredentials = JSON.parse(localStorage.getItem('broker_credentials') || '{}'); | |
| const connectionList: BrokerConnection[] = Object.keys(brokerCredentials).map(influencerId => { | |
| const credentials = brokerCredentials[influencerId]; | |
| return { | |
| id: influencerId, | |
| influencerId, | |
| brokerType: 'ZERODHA', | |
| credentials, | |
| isConnected: !!credentials.accessToken, | |
| lastConnected: credentials.accessToken ? new Date().toISOString() : undefined, | |
| permissions: { | |
| allowPositions: true, | |
| allowOrders: false, | |
| allowHoldings: true, | |
| }, | |
| }; | |
| }); | |
| setConnections(connectionList); | |
| } catch (err) { | |
| setError('Failed to load connections'); | |
| console.error('Error loading connections:', err); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const loadRegistrations = () => { | |
| try { | |
| const storedRegistrations = JSON.parse(localStorage.getItem('influencer_registrations') || '[]'); | |
| setRegistrations(storedRegistrations); | |
| } catch (err) { | |
| console.error('Error loading registrations:', err); | |
| setRegistrations([]); | |
| } | |
| }; | |
| const handleDisconnect = (influencerId: string) => { | |
| if (confirm('Are you sure you want to disconnect this broker account?')) { | |
| removeBrokerCredentials(influencerId); | |
| loadConnections(); | |
| } | |
| }; | |
| const handleClearAll = () => { | |
| if (confirm('Are you sure you want to clear all broker connections? This cannot be undone.')) { | |
| clearAllBrokerCredentials(); | |
| loadConnections(); | |
| } | |
| }; | |
| const handleRefresh = () => { | |
| loadConnections(); | |
| loadRegistrations(); | |
| }; | |
| const handleApproveRegistration = (registrationId: string) => { | |
| const updatedRegistrations = registrations.map(reg => { | |
| if (reg.id === registrationId) { | |
| return { | |
| ...reg, | |
| status: 'approved' as ApprovalStatus, | |
| reviewedAt: new Date().toISOString(), | |
| reviewedBy: 'admin', | |
| }; | |
| } | |
| return reg; | |
| }); | |
| setRegistrations(updatedRegistrations); | |
| localStorage.setItem('influencer_registrations', JSON.stringify(updatedRegistrations)); | |
| // Add to approved influencers list | |
| const approvedInfluencers = JSON.parse(localStorage.getItem('approved_influencers') || '[]'); | |
| const registration = updatedRegistrations.find(r => r.id === registrationId); | |
| if (registration) { | |
| approvedInfluencers.push({ | |
| id: registration.id, | |
| name: registration.name, | |
| email: registration.email, | |
| avatar: registration.avatar, | |
| bio: registration.bio, | |
| socialLinks: registration.socialLinks, | |
| tradingStyle: registration.tradingStyle, | |
| followers: registration.followers, | |
| isPrivate: registration.isPrivate, | |
| accessCode: registration.accessCode, | |
| }); | |
| localStorage.setItem('approved_influencers', JSON.stringify(approvedInfluencers)); | |
| } | |
| }; | |
| const handleRejectRegistration = (registrationId: string, reason: string) => { | |
| const updatedRegistrations = registrations.map(reg => { | |
| if (reg.id === registrationId) { | |
| return { | |
| ...reg, | |
| status: 'rejected' as ApprovalStatus, | |
| reviewedAt: new Date().toISOString(), | |
| reviewedBy: 'admin', | |
| rejectionReason: reason, | |
| }; | |
| } | |
| return reg; | |
| }); | |
| setRegistrations(updatedRegistrations); | |
| localStorage.setItem('influencer_registrations', JSON.stringify(updatedRegistrations)); | |
| }; | |
| const handleDeleteInfluencer = (influencerId: string) => { | |
| if (confirm('Are you sure you want to delete this influencer? This will remove their account, data, and all connections. This action cannot be undone.')) { | |
| // Remove from approved influencers | |
| const approvedInfluencers = JSON.parse(localStorage.getItem('approved_influencers') || '[]'); | |
| const updatedApproved = approvedInfluencers.filter((inf: any) => inf.id !== influencerId); | |
| localStorage.setItem('approved_influencers', JSON.stringify(updatedApproved)); | |
| // Remove from users | |
| const users = JSON.parse(localStorage.getItem('users') || '[]'); | |
| const updatedUsers = users.filter((user: any) => user.influencerId !== influencerId); | |
| localStorage.setItem('users', JSON.stringify(updatedUsers)); | |
| // Remove from registrations | |
| const existingRegistrations = JSON.parse(localStorage.getItem('influencer_registrations') || '[]'); | |
| const updatedRegistrations = existingRegistrations.filter((reg: any) => reg.id !== influencerId); | |
| localStorage.setItem('influencer_registrations', JSON.stringify(updatedRegistrations)); | |
| // Remove broker credentials | |
| removeBrokerCredentials(influencerId); | |
| // Refresh data | |
| loadRegistrations(); | |
| loadConnections(); | |
| } | |
| }; | |
| if (!isOpen) return null; | |
| return ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4"> | |
| <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} /> | |
| <div className="relative glassmorphism rounded-2xl p-8 w-full max-w-4xl max-h-[80vh] overflow-y-auto animate-slide-up"> | |
| <div className="flex items-center justify-between mb-6"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center"> | |
| <Settings className="w-5 h-5 text-white" /> | |
| </div> | |
| <div> | |
| <h2 className="text-2xl font-bold text-foreground">Broker Connections</h2> | |
| <p className="text-sm text-muted-foreground">Manage influencer broker integrations</p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={onClose} | |
| className="p-2 rounded-lg hover:bg-accent/50 transition-colors" | |
| > | |
| <XCircle className="w-5 h-5 text-muted-foreground" /> | |
| </button> | |
| </div> | |
| {error && ( | |
| <div className="mb-6 p-4 rounded-lg bg-red-500/10 border border-red-500/20 text-red-500 flex items-center gap-3"> | |
| <AlertCircle className="w-5 h-5" /> | |
| <span>{error}</span> | |
| </div> | |
| )} | |
| <div className="flex gap-3 mb-6"> | |
| <button | |
| onClick={() => setActiveTab('connections')} | |
| className={cn( | |
| 'flex items-center gap-2 px-4 py-2 rounded-lg transition-colors', | |
| activeTab === 'connections' | |
| ? 'bg-primary text-primary-foreground' | |
| : 'border border-border text-foreground hover:bg-accent/50' | |
| )} | |
| > | |
| <LinkIcon className="w-4 h-4" /> | |
| Broker Connections | |
| </button> | |
| <button | |
| onClick={() => setActiveTab('registrations')} | |
| className={cn( | |
| 'flex items-center gap-2 px-4 py-2 rounded-lg transition-colors', | |
| activeTab === 'registrations' | |
| ? 'bg-primary text-primary-foreground' | |
| : 'border border-border text-foreground hover:bg-accent/50' | |
| )} | |
| > | |
| <UserPlus className="w-4 h-4" /> | |
| Influencer Requests | |
| {registrations.filter(r => r.status === 'pending').length > 0 && ( | |
| <span className="w-5 h-5 rounded-full bg-red-500 text-white text-xs flex items-center justify-center"> | |
| {registrations.filter(r => r.status === 'pending').length} | |
| </span> | |
| )} | |
| </button> | |
| <div className="flex-1" /> | |
| <button | |
| onClick={handleRefresh} | |
| disabled={isLoading} | |
| className="flex items-center gap-2 px-4 py-2 rounded-lg border border-border text-foreground hover:bg-accent/50 transition-colors disabled:opacity-50" | |
| > | |
| <RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} /> | |
| Refresh | |
| </button> | |
| </div> | |
| {isLoading ? ( | |
| <div className="flex items-center justify-center py-12"> | |
| <div className="w-8 h-8 rounded-full border-2 border-primary border-t-transparent animate-spin" /> | |
| </div> | |
| ) : activeTab === 'connections' ? ( | |
| <> | |
| {connections.length === 0 ? ( | |
| <div className="text-center py-12"> | |
| <div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mx-auto mb-4"> | |
| <LinkIcon className="w-8 h-8 text-muted-foreground" /> | |
| </div> | |
| <h3 className="text-lg font-medium text-foreground mb-2">No Broker Connections</h3> | |
| <p className="text-muted-foreground"> | |
| Connect influencer Zerodha accounts to see them here | |
| </p> | |
| </div> | |
| ) : ( | |
| <div className="space-y-4"> | |
| {connections.map((connection) => { | |
| const influencer = influencers.find(inf => inf.id === connection.influencerId); | |
| return ( | |
| <div | |
| key={connection.id} | |
| className="bg-card/50 rounded-lg p-4 border border-border" | |
| > | |
| <div className="flex items-start justify-between"> | |
| <div className="flex items-center gap-4"> | |
| <div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-lg font-bold text-white"> | |
| {influencer?.avatar || 'NA'} | |
| </div> | |
| <div> | |
| <h3 className="font-semibold text-foreground"> | |
| {influencer?.name || 'Unknown Influencer'} | |
| </h3> | |
| <div className="flex items-center gap-2 mt-1"> | |
| <span className={cn( | |
| 'px-2 py-0.5 rounded text-xs font-medium', | |
| connection.isConnected | |
| ? 'bg-green-500/20 text-green-500' | |
| : 'bg-red-500/20 text-red-500' | |
| )}> | |
| {connection.isConnected ? 'Connected' : 'Disconnected'} | |
| </span> | |
| <span className="text-xs text-muted-foreground"> | |
| {connection.brokerType} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| <button | |
| onClick={() => handleDisconnect(connection.influencerId)} | |
| className="p-2 rounded-lg hover:bg-red-500/10 text-red-500 transition-colors" | |
| title="Disconnect" | |
| > | |
| <Trash2 className="w-4 h-4" /> | |
| </button> | |
| </div> | |
| <div className="mt-4 pt-4 border-t border-border"> | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> | |
| <div> | |
| <p className="text-muted-foreground mb-1">API Key</p> | |
| <p className="font-mono text-foreground"> | |
| {connection.credentials.apiKey.substring(0, 8)}... | |
| </p> | |
| </div> | |
| <div> | |
| <p className="text-muted-foreground mb-1">Status</p> | |
| <div className="flex items-center gap-1"> | |
| {connection.isConnected ? ( | |
| <CheckCircle className="w-4 h-4 text-green-500" /> | |
| ) : ( | |
| <XCircle className="w-4 h-4 text-red-500" /> | |
| )} | |
| <span className="text-foreground"> | |
| {connection.isConnected ? 'Active' : 'Inactive'} | |
| </span> | |
| </div> | |
| </div> | |
| <div> | |
| <p className="text-muted-foreground mb-1">Permissions</p> | |
| <div className="flex flex-wrap gap-1"> | |
| {connection.permissions.allowPositions && ( | |
| <span className="px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-500 text-xs"> | |
| Positions | |
| </span> | |
| )} | |
| {connection.permissions.allowHoldings && ( | |
| <span className="px-1.5 py-0.5 rounded bg-purple-500/20 text-purple-500 text-xs"> | |
| Holdings | |
| </span> | |
| )} | |
| {connection.permissions.allowOrders && ( | |
| <span className="px-1.5 py-0.5 rounded bg-orange-500/20 text-orange-500 text-xs"> | |
| Orders | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| <div> | |
| <p className="text-muted-foreground mb-1">Last Connected</p> | |
| <p className="text-foreground"> | |
| {connection.lastConnected | |
| ? new Date(connection.lastConnected).toLocaleDateString() | |
| : 'Never' | |
| } | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </> | |
| ) : ( | |
| <> | |
| {registrations.length === 0 ? ( | |
| <div className="text-center py-12"> | |
| <div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mx-auto mb-4"> | |
| <UserPlus className="w-8 h-8 text-muted-foreground" /> | |
| </div> | |
| <h3 className="text-lg font-medium text-foreground mb-2">No Registration Requests</h3> | |
| <p className="text-muted-foreground"> | |
| Influencer registration requests will appear here | |
| </p> | |
| </div> | |
| ) : ( | |
| <div className="space-y-4"> | |
| {registrations.map((registration) => ( | |
| <div | |
| key={registration.id} | |
| className="bg-card/50 rounded-lg p-4 border border-border" | |
| > | |
| <div className="flex items-start justify-between"> | |
| <div className="flex items-center gap-4"> | |
| <div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-lg font-bold text-white"> | |
| {registration.avatar || registration.name.charAt(0).toUpperCase()} | |
| </div> | |
| <div> | |
| <h3 className="font-semibold text-foreground"> | |
| {registration.name} | |
| </h3> | |
| <div className="flex items-center gap-2 mt-1"> | |
| <span className={cn( | |
| 'px-2 py-0.5 rounded text-xs font-medium', | |
| registration.status === 'pending' ? 'bg-yellow-500/20 text-yellow-500' : | |
| registration.status === 'approved' ? 'bg-green-500/20 text-green-500' : | |
| 'bg-red-500/20 text-red-500' | |
| )}> | |
| {registration.status.charAt(0).toUpperCase() + registration.status.slice(1)} | |
| </span> | |
| {registration.isPrivate && ( | |
| <span className="px-2 py-0.5 rounded bg-orange-500/20 text-orange-500 text-xs font-medium"> | |
| Private | |
| </span> | |
| )} | |
| </div> | |
| <p className="text-sm text-muted-foreground mt-1">{registration.email}</p> | |
| </div> | |
| </div> | |
| {registration.status === 'pending' && ( | |
| <div className="flex gap-2"> | |
| <button | |
| onClick={() => handleApproveRegistration(registration.id)} | |
| className="flex items-center gap-1 px-3 py-1.5 rounded-lg bg-green-500/20 text-green-500 hover:bg-green-500/30 transition-colors text-sm" | |
| > | |
| <CheckCircle className="w-4 h-4" /> | |
| Approve | |
| </button> | |
| <button | |
| onClick={() => { | |
| const reason = prompt('Enter rejection reason:'); | |
| if (reason) { | |
| handleRejectRegistration(registration.id, reason); | |
| } | |
| }} | |
| className="flex items-center gap-1 px-3 py-1.5 rounded-lg bg-red-500/20 text-red-500 hover:bg-red-500/30 transition-colors text-sm" | |
| > | |
| <XCircle className="w-4 h-4" /> | |
| Reject | |
| </button> | |
| </div> | |
| )} | |
| {registration.status === 'approved' && ( | |
| <button | |
| onClick={() => handleDeleteInfluencer(registration.id)} | |
| className="flex items-center gap-1 px-3 py-1.5 rounded-lg bg-red-500/20 text-red-500 hover:bg-red-500/30 transition-colors text-sm" | |
| title="Delete Influencer" | |
| > | |
| <Trash2 className="w-4 h-4" /> | |
| Delete | |
| </button> | |
| )} | |
| </div> | |
| <div className="mt-4 pt-4 border-t border-border"> | |
| <div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm"> | |
| <div> | |
| <p className="text-muted-foreground mb-1">Trading Style</p> | |
| <p className="text-foreground">{registration.tradingStyle || 'N/A'}</p> | |
| </div> | |
| <div> | |
| <p className="text-muted-foreground mb-1">Followers</p> | |
| <p className="text-foreground">{registration.followers?.toLocaleString() || '0'}</p> | |
| </div> | |
| <div> | |
| <p className="text-muted-foreground mb-1">Submitted</p> | |
| <p className="text-foreground"> | |
| {new Date(registration.submittedAt).toLocaleDateString()} | |
| </p> | |
| </div> | |
| </div> | |
| {registration.bio && ( | |
| <div className="mt-3"> | |
| <p className="text-muted-foreground mb-1">Bio</p> | |
| <p className="text-foreground text-sm">{registration.bio}</p> | |
| </div> | |
| )} | |
| {registration.rejectionReason && ( | |
| <div className="mt-3 p-3 rounded-lg bg-red-500/10 border border-red-500/20"> | |
| <p className="text-sm text-red-500"> | |
| <strong>Rejection Reason:</strong> {registration.rejectionReason} | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </> | |
| )} | |
| <div className="mt-6 pt-6 border-t border-border"> | |
| <div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4"> | |
| <div className="flex items-start gap-3"> | |
| <AlertCircle className="w-5 h-5 text-blue-500 mt-0.5 flex-shrink-0" /> | |
| <div> | |
| <h4 className="font-medium text-foreground mb-1">Security Note</h4> | |
| <p className="text-sm text-muted-foreground"> | |
| Broker credentials are stored locally in your browser. Never share your API credentials with anyone. | |
| Regularly review and revoke access for any suspicious connections. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |