import React, { useState } from 'react' import { createFileRoute, Outlet, useLocation, useNavigate } from '@tanstack/react-router' import { AppContainer, p as MyText } from 'web-components' import { trpc } from '@/lib/trpc-client' import { ErrorToast } from '@/services/toaster' import { useManualRefresh } from '@/lib/refresh-context' import { Image as ImageIcon, Plus, Pencil, Trash2, CircleCheck, CircleDot, Hourglass, X } from 'lucide-react' import type { Banner as SharedBanner } from '@packages/shared' export const Route = createFileRoute('/dashboard/banners')({ component: DashboardBanners, }) // Serialized banner row (API JSON carries string dates) anchored to the shared shape type Banner = Omit & { createdAt: string, lastUpdated: string } function DashboardBanners() { const location = useLocation() const navigate = useNavigate() const [refreshing, setRefreshing] = useState(false) // Edit mode state const [editMode, setEditMode] = useState(false) const [selectedSlot, setSelectedSlot] = useState(null) const [slotAssignments, setSlotAssignments] = useState<{ [slot: number]: number | null }>({}) const [originalAssignments, setOriginalAssignments] = useState<{ [slot: number]: number | null }>({}) const [saving, setSaving] = useState(false) // Real API calls const { data: bannersData, isLoading, error, refetch } = trpc.admin.banner.getBanners.useQuery() const deleteBannerMutation = trpc.admin.banner.deleteBanner.useMutation() const updateBannerMutation = trpc.admin.banner.updateBanner.useMutation() const emptySlotMutation = trpc.admin.banner.updateBanner.useMutation() const banners = bannersData?.banners || [] // Initialize slot assignments when banners load React.useEffect(() => { const assignments: { [slot: number]: number | null } = { 1: null, 2: null, 3: null, 4: null } banners.forEach((banner) => { if (banner.serialNum && banner.serialNum >= 1 && banner.serialNum <= 4) { assignments[banner.serialNum] = banner.id } }) setSlotAssignments(assignments) setOriginalAssignments({ ...assignments }) }, [bannersData]) const onRefresh = async () => { setRefreshing(true) await refetch() setRefreshing(false) } useManualRefresh(() => { onRefresh() }) if (location.pathname !== '/dashboard/banners') { return } // Slot and edit mode handlers const handleSlotClick = (slotNumber: number) => { setSelectedSlot(slotNumber) setEditMode(true) } const handleBannerSelect = (bannerId: number) => { if (!editMode || selectedSlot === null) return // Remove banner from any existing slot const newAssignments = { ...slotAssignments } Object.keys(newAssignments).forEach((slot) => { if (newAssignments[parseInt(slot)] === bannerId) { newAssignments[parseInt(slot)] = null } }) // Assign banner to selected slot newAssignments[selectedSlot] = bannerId setSlotAssignments(newAssignments) } const handleSave = async () => { setSaving(true) try { // Get banners that need to be updated const bannersToUpdate: { id: number, serialNum: number | null }[] = [] // Clear serial numbers for banners no longer in slots banners.forEach((banner) => { const currentSlot = banner.serialNum const assignedSlot = Object.keys(slotAssignments).find((slot) => slotAssignments[parseInt(slot)] === banner.id ) if (currentSlot !== (assignedSlot ? parseInt(assignedSlot) : null)) { bannersToUpdate.push({ id: banner.id, serialNum: assignedSlot ? parseInt(assignedSlot) : null }) } }) // Update banners that gained slots Object.keys(slotAssignments).forEach((slot) => { const slotNum = parseInt(slot) const bannerId = slotAssignments[slotNum] if (bannerId) { const banner = banners.find((b) => b.id === bannerId) if (banner && banner.serialNum !== slotNum) { bannersToUpdate.push({ id: bannerId, serialNum: slotNum }) } } }) // Execute updates await Promise.all( bannersToUpdate.map(({ id, serialNum }) => updateBannerMutation.mutateAsync({ id, serialNum }) ) ) setOriginalAssignments({ ...slotAssignments }) setEditMode(false) setSelectedSlot(null) await refetch() window.alert('Success: Slot assignments saved successfully') } catch (err) { ErrorToast('Failed to save slot assignments') } finally { setSaving(false) } } const handleCancel = () => { setSlotAssignments({ ...originalAssignments }) setEditMode(false) setSelectedSlot(null) } const handleEmptySlot = async () => { if (!selectedSlot || !slotAssignments[selectedSlot]) return const bannerId = slotAssignments[selectedSlot] const banner = banners.find((b) => b.id === bannerId) if (!banner) return try { // Update banner's serialNum to null to empty the slot await emptySlotMutation.mutateAsync({ id: banner.id, serialNum: null }) // Update local state setSlotAssignments((prev) => ({ ...prev, [selectedSlot]: null })) // Update original assignments for cancel functionality setOriginalAssignments((prev) => ({ ...prev, [selectedSlot]: null })) window.alert(`Success: Slot ${selectedSlot} has been emptied`) } catch (err) { ErrorToast('Failed to empty slot') } } // Helper function to get banner name by ID const getBannerNameById = (id: number | null) => { if (!id) return null const banner = banners.find((b) => b.id === id) return banner ? banner.name : null } const handleEdit = (banner: Banner) => { navigate({ to: '/dashboard/banners/$id/edit', params: { id: String(banner.id) } }) } const handleDelete = (id: number) => { if (!window.confirm('Delete Banner: Are you sure you want to delete this banner?')) return ;(async () => { try { await deleteBannerMutation.mutateAsync({ id }) refetch() window.alert('Success: Banner deleted') } catch (err) { ErrorToast('Failed to delete banner') } })() } const handleCreate = () => { navigate({ to: '/dashboard/banners/new' }) } if (isLoading) { return (
Loading banners...
) } if (error) { return (
Error loading banners
) } return (
{/* Slots Row */}
{editMode ? `Select banner for Slot ${selectedSlot}` : 'Banner Slots'}
{[1, 2, 3, 4].map((slotNum) => { const assignedBannerId = slotAssignments[slotNum] const bannerName = getBannerNameById(assignedBannerId) const isSelected = editMode && selectedSlot === slotNum return ( ) })}
{/* Action buttons in edit mode */} {editMode && (
{/* Save/Cancel buttons */}
{/* Empty Slot button */}
)}
{/* Banners List */}
{editMode && (
Tap a banner below to assign it to Slot {selectedSlot}
)} {banners.length === 0 ? (
No Banners Yet Start by creating your first banner using the button above.
) : ( banners.map((banner) => { const isAssignedToSlot = Object.values(slotAssignments).includes(banner.id) const isAssignedToSelectedSlot = slotAssignments[selectedSlot || 0] === banner.id const canInteract = editMode return (
handleBannerSelect(banner.id) : undefined} role={canInteract ? 'button' : undefined} tabIndex={canInteract ? 0 : undefined} onKeyDown={canInteract ? (e) => { if (e.key === 'Enter') handleBannerSelect(banner.id) } : undefined} className={`mb-3 w-full rounded-xl border bg-white p-4 text-left shadow-sm ${canInteract ? 'cursor-pointer' : ''} ${canInteract && isAssignedToSelectedSlot ? 'border-blue-500 bg-blue-50' : canInteract && isAssignedToSlot ? 'border-green-300 bg-green-50' : canInteract ? 'border-gray-200' : 'border-gray-100' }`} > {banner.name} {banner.name} {canInteract && isAssignedToSelectedSlot && ( )} {canInteract && isAssignedToSlot && !isAssignedToSelectedSlot && ( )} {!editMode && ( <> )} {banner.description && ( {banner.description} )} Created: {new Date(banner.createdAt).toLocaleDateString()} {banner.serialNum && banner.serialNum >= 1 && banner.serialNum <= 4 && ( Slot {banner.serialNum} )}
) }) )}
{/* Floating Action Button */}
) }