import { useState, useCallback, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { adminLandingsApi, LandingListItem, resolveLocaleDisplay } from '../api/landings'; import { useNotify } from '@/platform'; import { copyToClipboard } from '../utils/clipboard'; import { getApiErrorMessage } from '../utils/api-error'; import { usePlatform } from '../platform/hooks/usePlatform'; import { cn } from '../lib/utils'; import { BackIcon, PlusIcon, TrashIcon, GripIcon } from '../components/icons/LandingIcons'; import { EditIcon, CheckIcon, XIcon, GiftIcon, SaveIcon, CopyIcon, StatsChartIcon, } from '@/components/icons'; import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; // ============ Sortable Landing Card ============ interface SortableLandingCardProps { landing: LandingListItem; onEdit: () => void; onStats: () => void; onDelete: () => void; onToggle: () => void; onCopyUrl: () => void; isPendingDelete?: boolean; } function SortableLandingCard({ landing, onEdit, onStats, onDelete, onToggle, onCopyUrl, isPendingDelete, }: SortableLandingCardProps) { const { t } = useTranslation(); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: landing.id, }); const style: React.CSSProperties = { transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 50 : undefined, position: isDragging ? 'relative' : undefined, }; return (
{/* Drag handle */} {/* Content + Actions wrapper */}
{/* Top row: title/slug + actions (desktop) */}

{resolveLocaleDisplay(landing.title)}

{landing.slug} {landing.is_active ? ( {t('admin.landings.active')} ) : ( {t('admin.landings.inactive')} )} {landing.gift_enabled && ( )} {landing.has_active_discount && ( {t('admin.landings.discountActive', 'Discount')} )}
{landing.purchase_stats.total} {t('admin.landings.stats.created', 'created')} / {landing.purchase_stats.paid + landing.purchase_stats.delivered + landing.purchase_stats.pending_activation} {t('admin.landings.stats.paid', 'paid')}
{/* Actions: hidden on mobile, shown on desktop */}
{/* Actions: shown on mobile only */}
); } // ============ Main Page ============ export default function AdminLandings() { const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const notify = useNotify(); const { capabilities } = usePlatform(); const [localLandings, setLocalLandings] = useState([]); const [orderChanged, setOrderChanged] = useState(false); const [pendingDeleteId, setPendingDeleteId] = useState(null); const deleteTimeoutRef = useRef | undefined>(undefined); useEffect(() => { return () => { if (deleteTimeoutRef.current) clearTimeout(deleteTimeoutRef.current); }; }, []); // Queries const { data: landingsData, isLoading } = useQuery({ queryKey: ['admin-landings'], queryFn: () => adminLandingsApi.list(), staleTime: 30_000, }); // Sync fetched data to local state useEffect(() => { if (landingsData && !orderChanged) { setLocalLandings(landingsData); } }, [landingsData, orderChanged]); // Save order mutation const saveOrderMutation = useMutation({ mutationFn: (landingIds: number[]) => adminLandingsApi.reorder(landingIds), onSuccess: () => { setOrderChanged(false); queryClient.invalidateQueries({ queryKey: ['admin-landings'] }); notify.success(t('admin.landings.orderSaved')); }, onError: (err: unknown) => { notify.error(getApiErrorMessage(err, t('common.error'))); }, }); // Mutations const deleteMutation = useMutation({ mutationFn: adminLandingsApi.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-landings'] }); notify.success(t('admin.landings.deleted')); }, onError: (err: unknown) => { notify.error(getApiErrorMessage(err, t('common.error'))); }, }); const handleDelete = (landing: LandingListItem) => { if (pendingDeleteId === landing.id) { deleteMutation.mutate(landing.id); setPendingDeleteId(null); } else { setPendingDeleteId(landing.id); if (deleteTimeoutRef.current) clearTimeout(deleteTimeoutRef.current); deleteTimeoutRef.current = setTimeout( () => setPendingDeleteId((prev) => (prev === landing.id ? null : prev)), 3000, ); } }; const toggleMutation = useMutation({ mutationFn: adminLandingsApi.toggle, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-landings'] }); }, onError: (err: unknown) => { notify.error(getApiErrorMessage(err, t('common.error'))); }, }); const handleCopyUrl = async (slug: string) => { const url = `${window.location.origin}/buy/${slug}`; try { await copyToClipboard(url); notify.success(t('admin.landings.urlCopied')); } catch { // The clipboard adapter already tries the legacy execCommand fallback; // if we still failed, surface it so the admin doesn't think it worked. notify.error(t('common.error')); } }; // DnD sensors const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ); const handleDragEnd = useCallback((event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { setLocalLandings((prev) => { const oldIndex = prev.findIndex((l) => l.id === active.id); const newIndex = prev.findIndex((l) => l.id === over.id); if (oldIndex === -1 || newIndex === -1) return prev; return arrayMove(prev, oldIndex, newIndex); }); setOrderChanged(true); } }, []); const handleSaveOrder = () => { saveOrderMutation.mutate(localLandings.map((l) => l.id)); }; return (
{/* Header */}
{/* Show back button only on web, not in Telegram Mini App */} {!capabilities.hasBackButton && ( )}

{t('admin.landings.title')}

{orderChanged && ( )}
{/* Drag hint */}
{t('admin.tariffs.dragToReorder')}
{/* Landings List */} {isLoading ? (
) : localLandings.length === 0 ? (

{t('common.noData')}

) : ( l.id)} strategy={verticalListSortingStrategy} >
{localLandings.map((landing) => ( navigate(`/admin/landings/${landing.id}/edit`)} onStats={() => navigate(`/admin/landings/${landing.id}/stats`)} onDelete={() => handleDelete(landing)} onToggle={() => toggleMutation.mutate(landing.id)} onCopyUrl={() => handleCopyUrl(landing.slug)} isPendingDelete={pendingDeleteId === landing.id} /> ))}
)}
); }