Files
Imago/imago-admin/src/pages/gallery/ClientGallery.tsx
T
bruno 2f2a5e9b4a
CI / Lint & Format (push) Failing after 28s
CI / Tests (push) Has been skipped
CI / Security Scan (push) Failing after 8s
CI / Docker Build (push) Has been skipped
Add admin panel, WebSocket support, and API versioning
Introduce an admin portal (React + Nginx), WebSocket routing, and
API versioning middleware with `/api/v1/` prefix deprecation.
Add master API key authentication, new Prometheus metrics for AI
token consumption and active WebSockets, and extend S3 config
with a public endpoint URL. Update test paths and fixtures to
align with the new routing structure.
2026-06-22 11:25:22 -04:00

127 lines
5.6 KiB
TypeScript

import { useParams, Link } from 'react-router-dom';
import { PageLayout } from '@/components/layout/PageLayout';
import { PlanBadge } from '@/components/shared/PlanBadge';
import { StatusBadge } from '@/components/shared/StatusBadge';
import { Pagination } from '@/components/shared/Pagination';
import { EmptyState } from '@/components/shared/EmptyState';
import { ImageGridSkeleton } from '@/components/shared/Skeleton';
import { SearchInput } from '@/components/shared/SearchInput';
import { ImageLoader } from '@/components/shared/ImageLoader';
import { useImages } from '@/hooks/useImages';
import { useClient } from '@/hooks/useClients';
import { formatDate, formatBytes, truncate } from '@/utils/formatters';
import { ArrowLeft, ExternalLink, Images, Camera } from 'lucide-react';
import { useState, useCallback } from 'react';
export default function ClientGallery() {
const { clientId } = useParams<{ clientId: string }>();
const { data: client } = useClient(clientId);
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const { data, isLoading } = useImages({
client_id: clientId,
search: search || undefined,
page,
page_size: 20,
sort_by: 'uploaded_at',
sort_order: 'desc',
});
const images = data?.items ?? [];
const total = data?.total ?? 0;
const totalPages = data?.pages ?? 1;
const handleSearch = useCallback((value: string) => {
setSearch(value);
setPage(1);
}, []);
return (
<PageLayout
title={client ? `Images de "${client.name}"` : 'Galerie client'}
description={
client
? `${total} image${total !== 1 ? 's' : ''} · ${formatBytes(client.storage_used_bytes)} utilisés`
: undefined
}
actions={
<div className="flex items-center gap-2">
{client && <PlanBadge plan={client.plan} />}
{client && (
<Link to={`/admin/clients/${client.id}`} className="btn-ghost text-sm flex items-center gap-1">
Détail client <ExternalLink size={12} />
</Link>
)}
<Link to="/gallery" className="btn-ghost text-sm flex items-center gap-1">
<ArrowLeft size={14} /> Toutes les images
</Link>
</div>
}
>
{/* Search */}
<div className="mb-6">
<SearchInput
value={search}
onChange={handleSearch}
placeholder="Rechercher dans les images de ce client…"
className="max-w-md"
/>
</div>
{/* Grid */}
{isLoading ? (
<ImageGridSkeleton count={8} />
) : images.length === 0 ? (
<EmptyState
title="Aucune image"
description={search ? 'Aucun résultat pour cette recherche' : 'Ce client n\'a pas encore uploadé d\'images'}
icon={<Images size={32} className="text-content-muted" />}
/>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{images.map((img) => (
<Link
key={img.id}
to={`/gallery/image/${img.id}`}
className="card-compact group hover:border-border-light transition-all duration-200 hover:-translate-y-0.5"
>
<ImageLoader
imageId={img.id}
alt={img.original_name}
className="mb-3"
/>
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-2">
<p className="text-sm font-medium text-content-primary truncate">{truncate(img.original_name, 25)}</p>
<StatusBadge status={img.processing_status} />
</div>
{img.ai_tags?.length > 0 && (
<div className="flex flex-wrap gap-1">
{img.ai_tags?.slice(0, 3).map((tag) => (
<span key={tag} className="text-[10px] bg-surface-overlay text-content-muted px-1.5 py-0.5 rounded">{tag}</span>
))}
</div>
)}
<div className="flex items-center gap-2 text-xs text-content-muted">
<span>{formatDate(img.uploaded_at)}</span>
{img.exif_make && (
<span className="flex items-center gap-1"><Camera size={10} /> {img.exif_make}</span>
)}
</div>
</div>
</Link>
))}
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center mt-6">
<Pagination page={page} totalPages={totalPages} onPageChange={setPage} />
</div>
)}
</PageLayout>
);
}