feat: add Review Management module and UI layout fixes
This commit is contained in:
167
src/features/ad-control/components/EventPickerModal.tsx
Normal file
167
src/features/ad-control/components/EventPickerModal.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user