Update favicon and site title

This commit is contained in:
CycroftX
2026-02-03 20:30:11 +05:30
parent 61423f951a
commit cbc7cd1bd3
19 changed files with 3276 additions and 42 deletions

View File

@@ -0,0 +1,47 @@
import { useState } from 'react';
import { AppLayout } from '@/components/layout/AppLayout';
import { PartnerFilters } from './components/PartnerFilters';
import { PartnerCard } from './components/PartnerCard';
import { mockPartners } from '@/data/mockPartnerData';
export default function PartnerDirectory() {
const [searchQuery, setSearchQuery] = useState('');
const filteredPartners = mockPartners.filter(partner =>
partner.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
partner.type.toLowerCase().includes(searchQuery.toLowerCase()) ||
partner.primaryContact.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<AppLayout title="Partners">
<div className="p-6 max-w-[1600px] mx-auto space-y-8">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-bold tracking-tight">Partner Management</h1>
<p className="text-muted-foreground">Manage your relationships with venues, promoters, sponsors, and vendors.</p>
</div>
<div className="bg-card/30 p-1 rounded-xl glass-panel">
<PartnerFilters
onSearch={setSearchQuery}
onFilter={() => console.log('Filter clicked')}
onAdd={() => console.log('Add clicked')}
/>
</div>
{filteredPartners.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{filteredPartners.map(partner => (
<PartnerCard key={partner.id} partner={partner} />
))}
</div>
) : (
<div className="text-center py-20 bg-card/20 rounded-xl border border-dashed border-border/50">
<h3 className="text-lg font-medium text-foreground">No partners found</h3>
<p className="text-muted-foreground mt-2">Try adjusting your search or filters</p>
</div>
)}
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,277 @@
import { useParams } from 'react-router-dom';
import { AppLayout } from '@/components/layout/AppLayout';
import { mockPartners, mockDealTerms, mockLedger, mockDocuments } from '@/data/mockPartnerData';
import { StatusBadge, TypeBadge } from './components/PartnerBadges';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from '@/components/ui/button';
import { Calendar, Download, Edit, FileText, Mail, Phone, ExternalLink, Wallet } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
export default function PartnerProfile() {
const { id } = useParams<{ id: string }>();
// In a real app, fetch data based on ID
const partner = mockPartners.find(p => p.id === id) || mockPartners[0];
const dealTerms = mockDealTerms.filter(dt => dt.partnerId === partner.id);
const ledger = mockLedger.filter(l => l.partnerId === partner.id);
const documents = mockDocuments.filter(d => d.partnerId === partner.id);
if (!partner) return <div>Partner not found</div>;
return (
<AppLayout title={partner.name}>
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
{/* Header Profile */}
<div className="relative overflow-hidden rounded-2xl bg-card border border-border/50 shadow-lg group">
<div className="absolute inset-0 bg-gradient-to-r from-accent/5 to-transparent opacity-50" />
<div className="relative p-8 flex flex-col md:flex-row gap-8 items-start">
<div className="h-28 w-28 rounded-2xl bg-secondary flex items-center justify-center overflow-hidden border-2 border-border shadow-2xl">
{partner.logo ? (
<img src={partner.logo} alt={partner.name} className="h-full w-full object-cover" />
) : (
<span className="text-3xl font-bold text-muted-foreground">{partner.name.substring(0, 2)}</span>
)}
</div>
<div className="flex-1 space-y-4">
<div className="flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold tracking-tight text-foreground flex items-center gap-3">
{partner.name}
<StatusBadge status={partner.status} />
</h1>
<div className="flex items-center gap-4 mt-2 text-muted-foreground">
<TypeBadge type={partner.type} />
<span className="flex items-center gap-1 text-sm"><Calendar className="h-4 w-4" /> Joined {new Date(partner.joinedAt).toLocaleDateString()}</span>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" className="gap-2"><Edit className="h-4 w-4" /> Edit Profile</Button>
<Button className="bg-accent text-white gap-2"><Wallet className="h-4 w-4" /> New Settlement</Button>
</div>
</div>
<div className="flex flex-wrap gap-6 pt-4 border-t border-border/30">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-secondary/50 flex items-center justify-center text-primary">
<span className="text-xs font-bold">{partner.primaryContact.name.substring(0, 2)}</span>
</div>
<div>
<p className="text-sm font-medium">{partner.primaryContact.name}</p>
<p className="text-xs text-muted-foreground">{partner.primaryContact.role}</p>
</div>
</div>
<div className="flex items-center gap-4 text-sm text-foreground/80 bg-secondary/30 px-4 py-2 rounded-lg border border-border/30">
<a href={`mailto:${partner.primaryContact.email}`} className="flex items-center gap-2 hover:text-accent"><Mail className="h-4 w-4" /> {partner.primaryContact.email}</a>
{partner.primaryContact.phone && (
<span className="flex items-center gap-2 border-l border-border pl-4"><Phone className="h-4 w-4" /> {partner.primaryContact.phone}</span>
)}
</div>
</div>
</div>
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="neu-card p-4 flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wider">Total Revenue</p>
<p className="text-2xl font-bold mt-1">{partner.metrics.totalRevenue.toLocaleString()}</p>
</div>
<div className="h-10 w-10 rounded-full bg-success/10 flex items-center justify-center text-success">
<Wallet className="h-5 w-5" />
</div>
</div>
<div className="neu-card p-4 flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wider">Open Balance</p>
<p className="text-2xl font-bold mt-1">{partner.metrics.openBalance.toLocaleString()}</p>
</div>
<div className="h-10 w-10 rounded-full bg-warning/10 flex items-center justify-center text-warning">
<Wallet className="h-5 w-5" />
</div>
</div>
<div className="neu-card p-4 flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wider">Active Deals</p>
<p className="text-2xl font-bold mt-1">{partner.metrics.activeDeals}</p>
</div>
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
<FileText className="h-5 w-5" />
</div>
</div>
<div className="neu-card p-4 flex items-center justify-between">
<div>
<p className="text-xs text-muted-foreground font-medium uppercase tracking-wider">Events</p>
<p className="text-2xl font-bold mt-1">{partner.metrics.eventsCount}</p>
</div>
<div className="h-10 w-10 rounded-full bg-accent/10 flex items-center justify-center text-accent">
<Calendar className="h-5 w-5" />
</div>
</div>
</div>
{/* Tabs Content */}
<div className="neu-card min-h-[500px]">
<Tabs defaultValue="overview" className="w-full">
<div className="border-b border-border/40 px-6 pt-4">
<TabsList className="bg-transparent h-auto p-0 gap-6">
<TabsTrigger value="overview" className="tab-trigger pb-4 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-accent data-[state=active]:bg-transparent">Overview</TabsTrigger>
<TabsTrigger value="assignments" className="tab-trigger pb-4 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-accent data-[state=active]:bg-transparent">Assignments</TabsTrigger>
<TabsTrigger value="terms" className="tab-trigger pb-4 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-accent data-[state=active]:bg-transparent">Deal Terms</TabsTrigger>
<TabsTrigger value="finance" className="tab-trigger pb-4 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-accent data-[state=active]:bg-transparent">Financials</TabsTrigger>
<TabsTrigger value="docs" className="tab-trigger pb-4 rounded-none data-[state=active]:border-b-2 data-[state=active]:border-accent data-[state=active]:bg-transparent">Documents</TabsTrigger>
</TabsList>
</div>
<div className="p-6">
<TabsContent value="overview" className="space-y-6">
<div className="grid grid-cols-2 gap-8">
<div className="space-y-4">
<h3 className="font-semibold text-lg">Partner Details</h3>
<div className="grid grid-cols-2 gap-y-4 text-sm">
<span className="text-muted-foreground">Legal Name</span>
<span>{partner.companyDetails?.legalName || partner.name}</span>
<span className="text-muted-foreground">Tax ID</span>
<span>{partner.companyDetails?.taxId || '-'}</span>
<span className="text-muted-foreground">Website</span>
<a href={partner.companyDetails?.website} target="_blank" className="text-accent hover:underline flex items-center gap-1">{partner.companyDetails?.website || '-'} <ExternalLink className="h-3 w-3" /></a>
<span className="text-muted-foreground">Address</span>
<span>{partner.companyDetails?.address || '-'}</span>
</div>
</div>
<div className="space-y-4">
<h3 className="font-semibold text-lg">Tags & Notes</h3>
<div className="flex flex-wrap gap-2">
{partner.tags.map(tag => (
<Badge key={tag} variant="secondary" className="px-3 py-1">{tag}</Badge>
))}
</div>
<div className="p-4 bg-secondary/30 rounded-lg text-sm text-balance">
{partner.notes || "No notes added for this partner."}
</div>
</div>
</div>
</TabsContent>
<TabsContent value="finance">
<div className="flex justify-between items-center mb-6">
<h3 className="font-semibold text-lg">Ledger & Settlements</h3>
<Button variant="outline" size="sm" className="gap-2"><Download className="h-4 w-4" /> Export CSV</Button>
</div>
<div className="border border-border/50 rounded-lg overflow-hidden">
<table className="w-full text-sm text-left">
<thead className="bg-secondary/50 text-muted-foreground">
<tr>
<th className="p-3">Date</th>
<th className="p-3">Description</th>
<th className="p-3">Type</th>
<th className="p-3 text-right">Amount</th>
<th className="p-3 text-center">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-border/30">
{ledger.map(entry => (
<tr key={entry.id} className="hover:bg-accent/5">
<td className="p-3">{new Date(entry.createdAt).toLocaleDateString()}</td>
<td className="p-3">
<div className="font-medium">{entry.description}</div>
{entry.referenceId && <div className="text-xs text-muted-foreground">Ref: {entry.referenceId}</div>}
</td>
<td className="p-3"><Badge variant="outline">{entry.type}</Badge></td>
<td className={cn("p-3 text-right font-medium", entry.amount < 0 ? "text-error" : "text-success")}>
{entry.amount < 0 ? '-' : '+'}{Math.abs(entry.amount).toLocaleString()}
</td>
<td className="p-3 text-center">
<span className={cn("text-xs px-2 py-1 rounded-full border",
entry.status === 'Cleared' ? 'bg-success/10 border-success/20 text-success' :
entry.status === 'Pending' ? 'bg-warning/10 border-warning/20 text-warning' : 'bg-muted border-border'
)}>{entry.status}</span>
</td>
</tr>
))}
</tbody>
</table>
{ledger.length === 0 && <div className="p-8 text-center text-muted-foreground">No transactions found</div>}
</div>
</TabsContent>
<TabsContent value="docs">
<div className="flex justify-between items-center mb-6">
<h3 className="font-semibold text-lg">Contracts & Documents</h3>
<Button variant="outline" size="sm" className="gap-2"><Plus className="h-4 w-4" /> Upload Document</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{documents.map(doc => (
<div key={doc.id} className="p-4 border border-border/30 rounded-lg bg-card/50 flex items-start gap-3 hover:border-accent/40 transition-colors">
<div className="h-10 w-10 bg-secondary rounded-lg flex items-center justify-center text-muted-foreground">
<FileText className="h-5 w-5" />
</div>
<div className="flex-1 overflow-hidden">
<p className="font-medium truncate">{doc.name}</p>
<p className="text-xs text-muted-foreground capitalize">{doc.type} {doc.status}</p>
<p className="text-xs text-muted-foreground mt-1">Uploaded {new Date(doc.uploadedAt).toLocaleDateString()}</p>
</div>
<Button variant="ghost" size="icon" className="h-8 w-8"><Download className="h-4 w-4" /></Button>
</div>
))}
</div>
</TabsContent>
<TabsContent value="terms">
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-semibold text-lg">Active Deal Terms</h3>
<Button size="sm">Add New Term</Button>
</div>
{dealTerms.map(term => (
<div key={term.id} className="p-4 border border-border/50 rounded-xl bg-gradient-to-br from-card to-secondary/30">
<div className="flex justify-between items-start mb-2">
<div>
<h4 className="font-bold flex items-center gap-2">
{term.name}
<Badge variant="secondary" className="text-xs font-normal">v{term.version}</Badge>
</h4>
<p className="text-sm text-muted-foreground mt-1">Effective from {new Date(term.effectiveFrom).toLocaleDateString()}</p>
</div>
<Badge variant="outline" className="bg-success/5 border-success/20 text-success">{term.status}</Badge>
</div>
<div className="mt-4 p-3 bg-secondary/50 rounded-lg text-sm grid grid-cols-2 gap-4">
<div>
<span className="text-muted-foreground block text-xs uppercase">Type</span>
<span className="font-medium">{term.type}</span>
</div>
<div>
<span className="text-muted-foreground block text-xs uppercase">Parameters</span>
<span className="font-medium">
{term.type === 'RevenueShare' ? `${term.params.percentage}% Share` :
term.type === 'CommissionPerTicket' ? `${term.params.amount} per ticket` : 'Custom'}
</span>
</div>
</div>
</div>
))}
</div>
</TabsContent>
<TabsContent value="assignments">
<div className="p-8 text-center border-2 border-dashed border-border/50 rounded-xl">
<Calendar className="h-12 w-12 text-muted-foreground mx-auto mb-4 opacity-50" />
<h3 className="text-lg font-medium">No event assignments yet</h3>
<p className="text-muted-foreground mb-4">Assign this partner to an upcoming event</p>
<Button>Assign to Event</Button>
</div>
</TabsContent>
</div>
</Tabs>
</div>
</div>
</AppLayout>
);
}
function Plus({ className }: { className?: string }) {
return <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M5 12h14" /><path d="M12 5v14" /></svg>
}

View File

@@ -0,0 +1,35 @@
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import { PartnerStatus, PartnerType } from '@/types/partner';
export const StatusBadge = ({ status }: { status: PartnerStatus }) => {
const styles = {
Active: 'bg-success/10 text-success border-success/20',
Invited: 'bg-primary/10 text-primary border-primary/20',
Suspended: 'bg-error/10 text-error border-error/20',
Archived: 'bg-muted text-muted-foreground border-border',
};
return (
<Badge variant="outline" className={cn("font-medium", styles[status])}>
{status}
</Badge>
);
};
export const TypeBadge = ({ type }: { type: PartnerType }) => {
const styles = {
Venue: 'text-purple-400 border-purple-400/30 bg-purple-400/10',
Promoter: 'text-amber-400 border-amber-400/30 bg-amber-400/10',
Sponsor: 'text-emerald-400 border-emerald-400/30 bg-emerald-400/10',
Vendor: 'text-blue-400 border-blue-400/30 bg-blue-400/10',
Affiliate: 'text-pink-400 border-pink-400/30 bg-pink-400/10',
Other: 'text-gray-400 border-gray-400/30 bg-gray-400/10',
};
return (
<Badge variant="outline" className={cn("font-medium", styles[type])}>
{type}
</Badge>
);
};

View File

@@ -0,0 +1,82 @@
import { MoreHorizontal, ExternalLink, Calendar, Wallet } from 'lucide-react';
import { Partner } from '@/types/partner';
import { StatusBadge, TypeBadge } from './PartnerBadges';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { useNavigate } from 'react-router-dom';
interface PartnerCardProps {
partner: Partner;
}
export function PartnerCard({ partner }: PartnerCardProps) {
const navigate = useNavigate();
return (
<div
className="group relative neu-card p-5 hover:border-accent/50 transition-all duration-300 cursor-pointer"
onClick={() => navigate(`/partners/${partner.id}`)}
>
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-xl bg-secondary flex items-center justify-center overflow-hidden border border-border/50">
{partner.logo ? (
<img src={partner.logo} alt={partner.name} className="h-full w-full object-cover" />
) : (
<span className="text-lg font-bold text-muted-foreground">{partner.name.substring(0, 2)}</span>
)}
</div>
<div>
<h3 className="font-bold text-foreground group-hover:text-accent transition-colors">{partner.name}</h3>
<div className="flex gap-2 mt-1">
<TypeBadge type={partner.type} />
<StatusBadge status={partner.status} />
</div>
</div>
</div>
<div onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => navigate(`/partners/${partner.id}`)}>View Details</DropdownMenuItem>
<DropdownMenuItem>Assign to Event</DropdownMenuItem>
<DropdownMenuItem className="text-error">Suspend Partner</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mt-6 py-4 border-t border-border/30">
<div>
<p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
<Calendar className="h-3 w-3" /> Active Deals
</p>
<p className="font-semibold text-foreground">{partner.metrics.activeDeals}</p>
</div>
<div>
<p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
<Wallet className="h-3 w-3" /> Open Balance
</p>
<p className="font-semibold text-foreground">{partner.metrics.openBalance.toLocaleString()}</p>
</div>
</div>
<div className="mt-4 flex items-center justify-between text-xs text-muted-foreground">
<span>{partner.primaryContact.name}</span>
<span className="flex items-center gap-1 hover:text-accent">
View Portal <ExternalLink className="h-3 w-3" />
</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { Search, Filter, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
interface PartnerFiltersProps {
onSearch: (query: string) => void;
onFilter: () => void;
onAdd: () => void;
}
export function PartnerFilters({ onSearch, onFilter, onAdd }: PartnerFiltersProps) {
return (
<div className="flex flex-col sm:flex-row gap-4 justify-between items-center mb-6">
<div className="relative w-full sm:w-96">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search partners by name, type, or contact..."
className="pl-10 bg-secondary border-border/50 focus:border-accent"
onChange={(e) => onSearch(e.target.value)}
/>
</div>
<div className="flex gap-2 w-full sm:w-auto">
<Button variant="outline" onClick={onFilter} className="gap-2">
<Filter className="h-4 w-4" />
Filters
</Button>
<Button onClick={onAdd} className="gap-2 bg-accent text-white hover:bg-accent/90 shadow-lg shadow-accent/20">
<Plus className="h-4 w-4" />
Add Partner
</Button>
</div>
</div>
);
}