Files
eventify_command_center/src/features/ad-control/components/EventPickerModal.tsx

168 lines
8.5 KiB
TypeScript
Raw Normal View History

import { useState, useMemo } from 'react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Search, MapPin, Calendar, AlertTriangle, CheckCircle2,
ImageOff, Clock, Users,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import type { PickerEvent } from '@/lib/types/ad-control';
interface EventPickerModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
events: PickerEvent[];
onSelectEvent: (event: PickerEvent) => void;
alreadyPlacedEventIds: string[];
}
export function EventPickerModal({ open, onOpenChange, events, onSelectEvent, alreadyPlacedEventIds }: EventPickerModalProps) {
const [query, setQuery] = useState('');
const now = new Date();
const filtered = useMemo(() => {
if (!query.trim()) return events;
const q = query.toLowerCase();
return events.filter(e =>
e.title.toLowerCase().includes(q) ||
e.id.toLowerCase().includes(q) ||
e.organizer.toLowerCase().includes(q) ||
e.city.toLowerCase().includes(q) ||
e.category.toLowerCase().includes(q)
);
}, [events, query]);
const getWarnings = (event: PickerEvent): { label: string; severity: 'warning' | 'error' }[] => {
const warns: { label: string; severity: 'warning' | 'error' }[] = [];
if (event.approvalStatus === 'PENDING') warns.push({ label: 'Pending Approval', severity: 'warning' });
if (event.approvalStatus === 'REJECTED') warns.push({ label: 'Rejected', severity: 'error' });
if (new Date(event.endDate) < now) warns.push({ label: 'Event Ended', severity: 'error' });
if (!event.coverImage) warns.push({ label: 'No Cover Image', severity: 'warning' });
return warns;
};
const handleSelect = (event: PickerEvent) => {
const warnings = getWarnings(event);
const hasErrors = warnings.some(w => w.severity === 'error');
if (hasErrors) {
const msg = warnings.filter(w => w.severity === 'error').map(w => w.label).join(', ');
if (!confirm(`This event has issues: ${msg}. Proceed anyway?`)) return;
}
onSelectEvent(event);
onOpenChange(false);
setQuery('');
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col">
<DialogHeader>
<DialogTitle>Select Event</DialogTitle>
<DialogDescription>Search for an event to place on this surface.</DialogDescription>
</DialogHeader>
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search by name, ID, organizer, city, or category..."
className="pl-10"
autoFocus
/>
</div>
{/* Event List */}
<div className="flex-1 overflow-y-auto space-y-2 pr-1 min-h-0">
{filtered.length === 0 && (
<div className="text-center py-12 text-muted-foreground text-sm">
No events found matching "{query}"
</div>
)}
{filtered.map(event => {
const warnings = getWarnings(event);
const isPlaced = alreadyPlacedEventIds.includes(event.id);
const fillPercent = Math.round((event.ticketsSold / event.capacity) * 100);
return (
<button
key={event.id}
onClick={() => !isPlaced && handleSelect(event)}
disabled={isPlaced}
className={cn(
'w-full flex items-center gap-3 rounded-xl border p-3 text-left transition-all',
isPlaced
? 'opacity-50 cursor-not-allowed bg-muted/20'
: 'hover:shadow-md hover:border-primary/30 bg-card cursor-pointer',
)}
>
{/* Cover */}
{event.coverImage ? (
<img src={event.coverImage} alt="" className="h-16 w-24 rounded-lg object-cover flex-shrink-0" />
) : (
<div className="h-16 w-24 rounded-lg bg-muted flex items-center justify-center flex-shrink-0">
<ImageOff className="h-5 w-5 text-muted-foreground/30" />
</div>
)}
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<h4 className="font-semibold text-sm truncate">{event.title}</h4>
{event.approvalStatus === 'APPROVED' && (
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-500 flex-shrink-0" />
)}
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span className="flex items-center gap-1"><MapPin className="h-3 w-3" />{event.city}</span>
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{new Date(event.date).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}
</span>
<span>{event.organizer}</span>
</div>
<div className="flex items-center gap-2 mt-1">
<Badge variant="secondary" className="text-[10px] h-4">{event.category}</Badge>
<span className="text-[10px] text-muted-foreground flex items-center gap-1">
<Users className="h-3 w-3" /> {event.ticketsSold.toLocaleString()}/{event.capacity.toLocaleString()} ({fillPercent}%)
</span>
</div>
</div>
{/* Warnings */}
<div className="flex flex-col gap-1 flex-shrink-0 items-end">
{isPlaced && (
<Badge variant="secondary" className="text-[10px]">Already Placed</Badge>
)}
{warnings.map((w, i) => (
<Badge
key={i}
variant="outline"
className={cn(
'text-[10px] gap-1',
w.severity === 'error'
? 'bg-red-50 text-red-600 border-red-200'
: 'bg-amber-50 text-amber-600 border-amber-200'
)}
>
<AlertTriangle className="h-3 w-3" />
{w.label}
</Badge>
))}
</div>
</button>
);
})}
</div>
</DialogContent>
</Dialog>
);
}