Checkpoint: AAPDashboard mis à jour avec les 4 mêmes évolutions que VeilleDashboard :
1. Mode vignette par défaut 2. Alternance de couleurs en mode liste (pair=blanc, impair=slate-50) 3. Couleur de sélection au clic sur une ligne (bg-primary/10, titre en primary) 4. Bouton segmenté Non lus / Tous / Lus — Non lus par défaut 5. Message vide adapté selon le filtre actif 0 erreur TypeScript.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": "c7ed64d2",
|
"version": "d5ca5837",
|
||||||
"timestamp": 1782483733259
|
"timestamp": 1782484339948
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
List,
|
List,
|
||||||
Eye,
|
Eye,
|
||||||
|
EyeOff,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Calendar,
|
Calendar,
|
||||||
MapPin,
|
MapPin,
|
||||||
@@ -39,6 +40,7 @@ import { format, isPast, differenceInDays } from "date-fns";
|
|||||||
import { fr } from "date-fns/locale";
|
import { fr } from "date-fns/locale";
|
||||||
|
|
||||||
type AAPCategorie = "Handicap" | "PA" | "Enfance" | "Précarité" | "Sanitaire" | "Autre";
|
type AAPCategorie = "Handicap" | "PA" | "Enfance" | "Précarité" | "Sanitaire" | "Autre";
|
||||||
|
type ReadFilter = "unread" | "all" | "read";
|
||||||
|
|
||||||
interface AAPItem {
|
interface AAPItem {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -136,10 +138,17 @@ export default function AAPDashboard() {
|
|||||||
toast.error(`Erreur lors de la purge : ${err.message}`);
|
toast.error(`Erreur lors de la purge : ${err.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [viewMode, setViewMode] = useState<"list" | "grid">("list");
|
|
||||||
|
// ── État UI ─────────────────────────────────────────────────────────────────
|
||||||
|
// Mode vignette par défaut
|
||||||
|
const [viewMode, setViewMode] = useState<"list" | "grid">("grid");
|
||||||
const [activeTab, setActiveTab] = useState<AAPCategorie | "all">("all");
|
const [activeTab, setActiveTab] = useState<AAPCategorie | "all">("all");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [filterValues, setFilterValues] = useState<Record<string, string>>({});
|
const [filterValues, setFilterValues] = useState<Record<string, string>>({});
|
||||||
|
// Filtre Lu/Non lu — Non lu par défaut
|
||||||
|
const [readFilter, setReadFilter] = useState<ReadFilter>("unread");
|
||||||
|
// Ligne sélectionnée en mode liste
|
||||||
|
const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
|
||||||
|
|
||||||
const filtersQuery = trpc.aap.filters.useQuery();
|
const filtersQuery = trpc.aap.filters.useQuery();
|
||||||
|
|
||||||
@@ -168,10 +177,17 @@ export default function AAPDashboard() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const items = (itemsQuery.data?.items ?? []) as AAPItem[];
|
const allItems = (itemsQuery.data?.items ?? []) as AAPItem[];
|
||||||
const total = itemsQuery.data?.total ?? 0;
|
const total = itemsQuery.data?.total ?? 0;
|
||||||
const totalPages = Math.ceil(total / PAGE_SIZE);
|
const totalPages = Math.ceil(total / PAGE_SIZE);
|
||||||
|
|
||||||
|
// Filtrage Lu/Non lu côté client
|
||||||
|
const items = useMemo(() => {
|
||||||
|
if (readFilter === "unread") return allItems.filter((i) => !readIds.has(i.id));
|
||||||
|
if (readFilter === "read") return allItems.filter((i) => readIds.has(i.id));
|
||||||
|
return allItems;
|
||||||
|
}, [allItems, readIds, readFilter]);
|
||||||
|
|
||||||
const filterOptions = [
|
const filterOptions = [
|
||||||
{ key: "region", label: "Région", options: filtersQuery.data?.regions ?? [] },
|
{ key: "region", label: "Région", options: filtersQuery.data?.regions ?? [] },
|
||||||
{ key: "departement", label: "Département", options: filtersQuery.data?.departements ?? [] },
|
{ key: "departement", label: "Département", options: filtersQuery.data?.departements ?? [] },
|
||||||
@@ -183,6 +199,13 @@ export default function AAPDashboard() {
|
|||||||
|
|
||||||
const categories: AAPCategorie[] = ["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"];
|
const categories: AAPCategorie[] = ["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"];
|
||||||
|
|
||||||
|
const READ_FILTER_LABELS: Record<ReadFilter, string> = {
|
||||||
|
unread: "Non lus",
|
||||||
|
all: "Tous",
|
||||||
|
read: "Lus",
|
||||||
|
};
|
||||||
|
const READ_FILTER_ORDER: ReadFilter[] = ["unread", "all", "read"];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6 animate-fade-up">
|
<div className="p-6 space-y-6 animate-fade-up">
|
||||||
{/* En-tête */}
|
{/* En-tête */}
|
||||||
@@ -199,12 +222,14 @@ export default function AAPDashboard() {
|
|||||||
Handicap, Personnes Âgées, Enfance, Précarité, Sanitaire et Autre
|
Handicap, Personnes Âgées, Enfance, Précarité, Sanitaire et Autre
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<Button variant="outline" size="sm" onClick={() => markAllAsReadMutation.mutate()} disabled={markAllAsReadMutation.isPending} className="gap-2 text-muted-foreground">
|
<Button variant="outline" size="sm" onClick={() => markAllAsReadMutation.mutate()} disabled={markAllAsReadMutation.isPending} className="gap-2 text-muted-foreground">
|
||||||
|
<Eye size={15} />
|
||||||
Tout marquer comme lu
|
Tout marquer comme lu
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{/* Boutons mode d'affichage */}
|
||||||
<Button variant={viewMode === "list" ? "default" : "outline"} size="sm" onClick={() => setViewMode("list")} className="gap-2">
|
<Button variant={viewMode === "list" ? "default" : "outline"} size="sm" onClick={() => setViewMode("list")} className="gap-2">
|
||||||
<List size={15} />Liste
|
<List size={15} />Liste
|
||||||
</Button>
|
</Button>
|
||||||
@@ -227,8 +252,8 @@ export default function AAPDashboard() {
|
|||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription asChild>
|
<AlertDialogDescription asChild>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p>Cette action va <strong>supprimer définitivement</strong> tous les appels à projets (Handicap, PA, Enfance, Précarité, Sanitaire et Autre).</p>
|
<p>Cette action va <strong>supprimer définitivement</strong> tous les appels à projets.</p>
|
||||||
<p className="text-destructive font-medium">Cette opération est irréversible. Les données ne pourront pas être récupérées.</p>
|
<p className="text-destructive font-medium">Cette opération est irréversible.</p>
|
||||||
</div>
|
</div>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
@@ -248,7 +273,8 @@ export default function AAPDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Onglets */}
|
{/* Onglets catégories + bouton Lu/Non lu */}
|
||||||
|
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||||
<Tabs value={activeTab} onValueChange={(v) => { setActiveTab(v as AAPCategorie | "all"); setPage(1); }}>
|
<Tabs value={activeTab} onValueChange={(v) => { setActiveTab(v as AAPCategorie | "all"); setPage(1); }}>
|
||||||
<TabsList className="bg-muted/50 flex-wrap h-auto gap-1">
|
<TabsList className="bg-muted/50 flex-wrap h-auto gap-1">
|
||||||
<TabsTrigger value="all">Tous</TabsTrigger>
|
<TabsTrigger value="all">Tous</TabsTrigger>
|
||||||
@@ -258,6 +284,30 @@ export default function AAPDashboard() {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
{/* Bouton segmenté Lu / Non lu */}
|
||||||
|
<div className="flex items-center rounded-lg border border-border overflow-hidden shadow-sm">
|
||||||
|
{READ_FILTER_ORDER.map((f) => (
|
||||||
|
<button
|
||||||
|
key={f}
|
||||||
|
onClick={() => setReadFilter(f)}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 text-xs font-medium transition-colors flex items-center gap-1.5",
|
||||||
|
readFilter === f
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "bg-background text-muted-foreground hover:bg-muted/50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{f === "unread" && <EyeOff size={12} />}
|
||||||
|
{f === "read" && <Eye size={12} />}
|
||||||
|
{READ_FILTER_LABELS[f]}
|
||||||
|
{f === "unread" && unreadCount > 0 && (
|
||||||
|
<span className="ml-0.5 inline-flex items-center justify-center min-w-[16px] h-4 px-1 rounded-full bg-primary-foreground/20 text-[10px] font-bold">{unreadCount}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Filtres */}
|
{/* Filtres */}
|
||||||
<FilterBar
|
<FilterBar
|
||||||
filters={filterOptions}
|
filters={filterOptions}
|
||||||
@@ -276,11 +326,23 @@ export default function AAPDashboard() {
|
|||||||
) : items.length === 0 ? (
|
) : items.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-24 text-center">
|
<div className="flex flex-col items-center justify-center py-24 text-center">
|
||||||
<Target size={48} className="text-muted-foreground/30 mb-4" />
|
<Target size={48} className="text-muted-foreground/30 mb-4" />
|
||||||
<p className="text-muted-foreground font-medium">Aucun appel à projets trouvé</p>
|
<p className="text-muted-foreground font-medium">
|
||||||
<p className="text-muted-foreground/60 text-sm mt-1">Modifiez vos filtres ou importez des données</p>
|
{readFilter === "unread" ? "Aucun appel à projets non lu" : "Aucun appel à projets trouvé"}
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground/60 text-sm mt-1">
|
||||||
|
{readFilter === "unread"
|
||||||
|
? "Tous les appels à projets ont été lus, ou modifiez le filtre Lu/Non lu"
|
||||||
|
: "Modifiez vos filtres ou importez des données"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === "list" ? (
|
) : viewMode === "list" ? (
|
||||||
<AAPListView items={items} readIds={readIds} onMarkRead={(id) => markAsReadMutation.mutate({ articleId: id })} />
|
<AAPListView
|
||||||
|
items={items}
|
||||||
|
readIds={readIds}
|
||||||
|
onMarkRead={(id) => markAsReadMutation.mutate({ articleId: id })}
|
||||||
|
selectedRowId={selectedRowId}
|
||||||
|
onSelectRow={setSelectedRowId}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<AAPGridView items={items} readIds={readIds} onMarkRead={(id) => markAsReadMutation.mutate({ articleId: id })} />
|
<AAPGridView items={items} readIds={readIds} onMarkRead={(id) => markAsReadMutation.mutate({ articleId: id })} />
|
||||||
)}
|
)}
|
||||||
@@ -303,13 +365,25 @@ export default function AAPDashboard() {
|
|||||||
|
|
||||||
// ─── Vue Liste ────────────────────────────────────────────────────────────────
|
// ─── Vue Liste ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function AAPListView({ items, readIds, onMarkRead }: { items: AAPItem[]; readIds: Set<number>; onMarkRead: (id: number) => void }) {
|
function AAPListView({
|
||||||
|
items,
|
||||||
|
readIds,
|
||||||
|
onMarkRead,
|
||||||
|
selectedRowId,
|
||||||
|
onSelectRow,
|
||||||
|
}: {
|
||||||
|
items: AAPItem[];
|
||||||
|
readIds: Set<number>;
|
||||||
|
onMarkRead: (id: number) => void;
|
||||||
|
selectedRowId: number | null;
|
||||||
|
onSelectRow: (id: number | null) => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border overflow-hidden shadow-sm">
|
<div className="rounded-xl border border-border overflow-hidden shadow-sm">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-muted/50 border-b border-border">
|
<tr className="bg-muted/60 border-b border-border">
|
||||||
<th className="text-left px-4 py-3 font-semibold text-muted-foreground w-8">#</th>
|
<th className="text-left px-4 py-3 font-semibold text-muted-foreground w-8">#</th>
|
||||||
<th className="text-left px-4 py-3 font-semibold text-muted-foreground">Titre</th>
|
<th className="text-left px-4 py-3 font-semibold text-muted-foreground">Titre</th>
|
||||||
<th className="text-left px-4 py-3 font-semibold text-muted-foreground w-28">Catégorie</th>
|
<th className="text-left px-4 py-3 font-semibold text-muted-foreground w-28">Catégorie</th>
|
||||||
@@ -320,46 +394,85 @@ function AAPListView({ items, readIds, onMarkRead }: { items: AAPItem[]; readIds
|
|||||||
<th className="text-left px-4 py-3 font-semibold text-muted-foreground w-16">Lien</th>
|
<th className="text-left px-4 py-3 font-semibold text-muted-foreground w-16">Lien</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-border">
|
<tbody>
|
||||||
{items.map((item, idx) => (
|
{items.map((item, idx) => {
|
||||||
<tr key={item.id} className={cn("hover:bg-muted/30 transition-colors border-l-4", CAT_ACCENT[item.iaCategorie || item.categorie] || "border-l-transparent", !readIds.has(item.id) && "bg-blue-50/30")}>
|
const isSelected = selectedRowId === item.id;
|
||||||
|
const isRead = readIds.has(item.id);
|
||||||
|
const isEven = idx % 2 === 0;
|
||||||
|
const catKey = item.iaCategorie || item.categorie;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => onSelectRow(isSelected ? null : item.id)}
|
||||||
|
className={cn(
|
||||||
|
"border-b border-border/50 border-l-4 cursor-pointer transition-colors",
|
||||||
|
CAT_ACCENT[catKey] || "border-l-transparent",
|
||||||
|
isSelected
|
||||||
|
? "bg-primary/10 hover:bg-primary/15"
|
||||||
|
: isEven
|
||||||
|
? "bg-white hover:bg-primary/5"
|
||||||
|
: "bg-slate-50/80 hover:bg-primary/5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<td className="px-4 py-3 text-muted-foreground/50 text-xs">{idx + 1}</td>
|
<td className="px-4 py-3 text-muted-foreground/50 text-xs">{idx + 1}</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex items-start gap-2 max-w-sm">
|
<div className="flex items-start gap-2 max-w-sm">
|
||||||
{!readIds.has(item.id) && (
|
{!isRead && (
|
||||||
<span className="mt-1.5 w-2 h-2 rounded-full bg-primary flex-shrink-0" title="Non lu" />
|
<span className="mt-1.5 w-2 h-2 rounded-full bg-primary flex-shrink-0" title="Non lu" />
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className={cn("font-medium line-clamp-2 leading-snug", readIds.has(item.id) ? "text-muted-foreground" : "text-foreground")}>{item.titre}</p>
|
<p className={cn(
|
||||||
{item.iaResume && <p className="text-xs text-muted-foreground mt-1 line-clamp-2">{item.iaResume}</p>}
|
"font-medium line-clamp-2 leading-snug",
|
||||||
|
isRead ? "text-muted-foreground" : "text-foreground",
|
||||||
|
isSelected && "text-primary font-semibold"
|
||||||
|
)}>
|
||||||
|
{item.titre}
|
||||||
|
</p>
|
||||||
|
{item.iaResume && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">{item.iaResume}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<Badge variant="outline" className={cn("text-xs", CAT_COLORS[item.iaCategorie || item.categorie])}>
|
<Badge variant="outline" className={cn("text-xs", CAT_COLORS[catKey])}>
|
||||||
{item.iaCategorie || item.categorie}
|
{catKey}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-muted-foreground text-xs">{item.region || "—"}</td>
|
<td className="px-4 py-3 text-muted-foreground text-xs">{item.region || "—"}</td>
|
||||||
<td className="px-4 py-3 text-muted-foreground text-xs">{item.departement || "—"}</td>
|
<td className="px-4 py-3 text-muted-foreground text-xs">{item.departement || "—"}</td>
|
||||||
<td className="px-4 py-3 text-muted-foreground text-xs whitespace-nowrap">{formatDate(item.datePublication) || "—"}</td>
|
<td className="px-4 py-3 text-muted-foreground text-xs whitespace-nowrap">
|
||||||
|
{formatDate(item.datePublication) || "—"}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-3"><ClotureStatus date={item.dateCloture} /></td>
|
<td className="px-4 py-3"><ClotureStatus date={item.dateCloture} /></td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{!readIds.has(item.id) && (
|
{!isRead && (
|
||||||
<button onClick={() => onMarkRead(item.id)} className="text-muted-foreground hover:text-primary transition-colors" title="Marquer comme lu">
|
<button
|
||||||
|
onClick={() => onMarkRead(item.id)}
|
||||||
|
className="inline-flex items-center justify-center w-7 h-7 rounded-md bg-blue-50 text-blue-600 hover:bg-blue-100 hover:text-blue-700 transition-colors border border-blue-200"
|
||||||
|
title="Marquer comme lu"
|
||||||
|
>
|
||||||
<Eye size={13} />
|
<Eye size={13} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{item.lien && (
|
{item.lien && (
|
||||||
<a href={item.lien} target="_blank" rel="noopener noreferrer" className="text-accent hover:text-accent/80 transition-colors">
|
<a
|
||||||
<ExternalLink size={15} />
|
href={item.lien}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center justify-center w-7 h-7 rounded-md bg-emerald-50 text-emerald-600 hover:bg-emerald-100 hover:text-emerald-700 transition-colors border border-emerald-200"
|
||||||
|
title="Ouvrir la source"
|
||||||
|
>
|
||||||
|
<ExternalLink size={13} />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -369,28 +482,71 @@ function AAPListView({ items, readIds, onMarkRead }: { items: AAPItem[]; readIds
|
|||||||
|
|
||||||
// ─── Vue Vignettes ────────────────────────────────────────────────────────────
|
// ─── Vue Vignettes ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function AAPGridView({ items, readIds, onMarkRead }: { items: AAPItem[]; readIds: Set<number>; onMarkRead: (id: number) => void }) {
|
function AAPGridView({
|
||||||
|
items,
|
||||||
|
readIds,
|
||||||
|
onMarkRead,
|
||||||
|
}: {
|
||||||
|
items: AAPItem[];
|
||||||
|
readIds: Set<number>;
|
||||||
|
onMarkRead: (id: number) => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||||
{items.map((item) => (
|
{items.map((item) => {
|
||||||
<Card key={item.id} className={cn("group hover:shadow-md transition-all duration-200 border-border overflow-hidden border-l-4", CAT_ACCENT[item.iaCategorie || item.categorie] || "", !readIds.has(item.id) && "ring-1 ring-primary/20")}>
|
const catKey = item.iaCategorie || item.categorie;
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={item.id}
|
||||||
|
className={cn(
|
||||||
|
"group hover:shadow-md transition-all duration-200 border-border overflow-hidden border-l-4",
|
||||||
|
CAT_ACCENT[catKey] || "",
|
||||||
|
!readIds.has(item.id) && "ring-1 ring-primary/20"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<CardHeader className="pb-2 pt-4 px-4">
|
<CardHeader className="pb-2 pt-4 px-4">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<Badge variant="outline" className={cn("text-xs flex-shrink-0", CAT_COLORS[item.iaCategorie || item.categorie])}>
|
<Badge variant="outline" className={cn("text-xs flex-shrink-0", CAT_COLORS[catKey])}>
|
||||||
{item.iaCategorie || item.categorie}
|
{catKey}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||||
|
{!readIds.has(item.id) && (
|
||||||
|
<button
|
||||||
|
onClick={() => onMarkRead(item.id)}
|
||||||
|
className="text-muted-foreground hover:text-primary transition-colors"
|
||||||
|
title="Marquer comme lu"
|
||||||
|
>
|
||||||
|
<Eye size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{item.lien && (
|
{item.lien && (
|
||||||
<a href={item.lien} target="_blank" rel="noopener noreferrer" className="text-muted-foreground hover:text-accent transition-colors flex-shrink-0">
|
<a
|
||||||
|
href={item.lien}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-muted-foreground hover:text-accent transition-colors"
|
||||||
|
>
|
||||||
<ExternalLink size={14} />
|
<ExternalLink size={14} />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex items-start gap-1.5 mt-2">
|
<div className="flex items-start gap-1.5 mt-2">
|
||||||
{!readIds.has(item.id) && <span className="mt-1 w-2 h-2 rounded-full bg-primary flex-shrink-0" title="Non lu" />}
|
{!readIds.has(item.id) && (
|
||||||
<h3 className={cn("font-semibold text-sm leading-snug line-clamp-3", readIds.has(item.id) ? "text-muted-foreground" : "text-foreground")}>{item.titre}</h3>
|
<span className="mt-1 w-2 h-2 rounded-full bg-primary flex-shrink-0" title="Non lu" />
|
||||||
|
)}
|
||||||
|
<h3 className={cn(
|
||||||
|
"font-semibold text-sm leading-snug line-clamp-3",
|
||||||
|
readIds.has(item.id) ? "text-muted-foreground" : "text-foreground"
|
||||||
|
)}>
|
||||||
|
{item.titre}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-4 pb-4 space-y-2">
|
<CardContent className="px-4 pb-4 space-y-2">
|
||||||
|
{item.iaResume && (
|
||||||
|
<p className="text-xs text-muted-foreground line-clamp-3 leading-relaxed">{item.iaResume}</p>
|
||||||
|
)}
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{item.region && (
|
{item.region && (
|
||||||
<span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-violet-50 text-violet-700 border border-violet-200">
|
<span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-violet-50 text-violet-700 border border-violet-200">
|
||||||
@@ -414,7 +570,8 @@ function AAPGridView({ items, readIds, onMarkRead }: { items: AAPItem[]; readIds
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
2
todo.md
2
todo.md
@@ -169,7 +169,7 @@
|
|||||||
- [x] rssEngine.ts : même filtre avant insertion aap_items
|
- [x] rssEngine.ts : même filtre avant insertion aap_items
|
||||||
- [x] 21 tests passés, 0 erreur TypeScript
|
- [x] 21 tests passés, 0 erreur TypeScript
|
||||||
- [x] Déployer en recette
|
- [x] Déployer en recette
|
||||||
- [ ] Déployer en production
|
- [ ] Déployer en production (en attente validation recette)
|
||||||
- [x] Mode vignette par défaut
|
- [x] Mode vignette par défaut
|
||||||
- [x] Alternance de couleurs en mode liste (pair=blanc, impair=gris clair)
|
- [x] Alternance de couleurs en mode liste (pair=blanc, impair=gris clair)
|
||||||
- [x] Couleur de sélection au clic sur une ligne (bg-primary/10)
|
- [x] Couleur de sélection au clic sur une ligne (bg-primary/10)
|
||||||
|
|||||||
Reference in New Issue
Block a user