tradewithlive / src /components /AdminPanel.tsx
SherlockHolmes786's picture
Upload 27 files
e9a8d0a verified
Raw History Blame Contribute Delete
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>
);
};