import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Link, useNavigate } from 'react-router-dom';
import toast from 'react-hot-toast';
import { Plus, RefreshCw, Play, Power, AlertTriangle, CheckCircle2, Clock, Pause } from 'lucide-react';
import { api } from '../../lib/api-client';
import { AppShell, PageHeader, PageBody } from '../../layout/AppShell';
import { Button, Badge, Card, Skeleton, EmptyState } from '../../components/ui/primitives';
import { Table, THead, TBody, TR, TH, TD } from '../../components/ui/table';
import type { Feed } from './types';
function formatDate(iso: string | null) {
if (!iso) return '—';
const d = new Date(iso);
const now = new Date();
const diff = (now.getTime() - d.getTime()) / 1000;
if (diff < 60) return 'net';
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
if (diff < 86400) return `${Math.floor(diff / 3600)}u`;
return `${Math.floor(diff / 86400)}d`;
}
function statusBadge(feed: Feed) {
if (!feed.is_active) return Uit;
if (feed.consecutive_failures >= 3) return Fout;
if (feed.last_error && feed.consecutive_failures > 0) return Waarschuwing;
if (!feed.last_synced_at) return Nieuw;
return Actief;
}
function HealthDot({ feed }: { feed: Feed }) {
let color = 'bg-[#666]';
let Icon = Pause;
if (feed.is_active) {
if (feed.consecutive_failures >= 3) {
color = 'bg-[#FF3B3B]';
Icon = AlertTriangle;
} else if (feed.consecutive_failures > 0) {
color = 'bg-[#FFA500]';
Icon = AlertTriangle;
} else if (feed.last_success_at) {
color = 'bg-[#00FF6A]';
Icon = CheckCircle2;
} else {
color = 'bg-[#3B82F6]';
Icon = Clock;
}
}
return (
);
}
export function FeedsList() {
const qc = useQueryClient();
const navigate = useNavigate();
const [syncingAll, setSyncingAll] = useState(false);
const { data, isLoading } = useQuery({
queryKey: ['feeds'],
queryFn: () => api.get<{ feeds: Feed[] }>('/feeds'),
refetchInterval: 10000,
});
const syncMut = useMutation({
mutationFn: (id: number) => api.post<{ jobId: number }>(`/feeds/${id}/sync`),
onSuccess: (res) => {
toast.success(`Sync gestart (job #${res.jobId})`);
qc.invalidateQueries({ queryKey: ['feeds'] });
},
onError: (e: Error) => toast.error(e.message),
});
const toggleMut = useMutation({
mutationFn: ({ id, is_active }: { id: number; is_active: boolean }) =>
api.put<{ feed: Feed }>(`/feeds/${id}`, { is_active }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['feeds'] });
toast.success('Feed bijgewerkt');
},
onError: (e: Error) => toast.error(e.message),
});
async function syncAll() {
setSyncingAll(true);
try {
const res = await api.post<{ enqueued: number }>('/feeds/sync-all');
toast.success(`${res.enqueued} feeds in de wachtrij`);
qc.invalidateQueries({ queryKey: ['feeds'] });
} catch (e) {
toast.error(e instanceof Error ? e.message : 'Sync all mislukt');
} finally {
setSyncingAll(false);
}
}
const feeds = data?.feeds ?? [];
return (
>
}
/>
{isLoading ? (
{[0, 1, 2, 3].map((i) => (
))}
) : feeds.length === 0 ? (
navigate('/cms/feeds/new')}>
Nieuwe feed
}
/>
) : (
| Status |
Naam |
Provider |
Producten |
Laatste sync |
Status |
Acties |
{feeds.map((feed) => (
|
|
{feed.name}
{feed.last_error && feed.consecutive_failures > 0 && (
{feed.last_error}
)}
|
{feed.provider_name ?? feed.provider_slug ?? '—'}
|
{feed.total_products?.toLocaleString('nl-NL') ?? '0'}
|
{formatDate(feed.last_synced_at)} |
{statusBadge(feed)} |
|
))}
)}
);
}