feat: Phase 4 — theme badge on synthesis cards + sort by date/theme

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
master
oabrivard 3 months ago
parent ed29e74b1b
commit d2f98dc66f

@ -63,6 +63,10 @@ const fr = {
'home.deleteError': 'Erreur lors de la suppression de la synthese.',
'home.generationInProgress': 'Une generation est en cours...',
'home.viewProgress': 'Voir la progression',
'home.sortBy': 'Trier par',
'home.sortDate': 'Date',
'home.sortTheme': 'Theme',
'home.deletedTheme': 'Theme supprime',
// Generate
'generate.title': 'Generer la Synthese Hebdomadaire',

@ -1,6 +1,7 @@
import {
type Component,
createSignal,
createMemo,
onMount,
onCleanup,
Show,
@ -32,6 +33,7 @@ const Home: Component = () => {
const [error, setError] = createSignal<string | null>(null);
const [deletingId, setDeletingId] = createSignal<string | null>(null);
const [deleteTimers, setDeleteTimers] = createSignal<Record<string, ReturnType<typeof setTimeout>>>({});
const [sortBy, setSortBy] = createSignal<'date' | 'theme'>('date');
onCleanup(() => {
const timers = deleteTimers();
@ -98,83 +100,58 @@ const Home: Component = () => {
const hasInProgress = () =>
syntheses().some((s) => s.status === 'in_progress');
return (
<Show when={!loading()} fallback={<LoadingSpinner />}>
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex justify-between items-center mb-8">
<div>
<h1 class="text-3xl font-bold text-gray-900">{t('home.title')}</h1>
<p class="mt-2 text-sm text-gray-500">{t('home.subtitle')}</p>
</div>
<A
href="/generate"
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<Plus class="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
{t('home.newSynthesis')}
</A>
</div>
/** Groups syntheses by theme_name, null theme goes last. Within each group: newest first. */
const groupedByTheme = createMemo(() => {
const sorted = [...syntheses()].sort(
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
);
{/* In-progress banner */}
<Show when={hasInProgress()}>
<div class="mb-6 bg-indigo-50 border border-indigo-200 rounded-lg p-4 flex items-center justify-between">
<div class="flex items-center">
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-indigo-600 mr-3" />
<span class="text-sm font-medium text-indigo-800">
{t('home.generationInProgress')}
</span>
</div>
<A
href="/generate"
class="text-sm font-medium text-indigo-600 hover:text-indigo-800"
>
{t('home.viewProgress')}
</A>
</div>
</Show>
const map = new Map<string | null, SynthesisListItem[]>();
for (const s of sorted) {
const key = s.theme_name ?? null;
if (!map.has(key)) map.set(key, []);
map.get(key)!.push(s);
}
{/* Error display */}
<Show when={error()}>
<div class="mb-6 bg-red-50 border border-red-200 rounded-md p-4 text-sm text-red-800">
{error()}
</div>
</Show>
// Named themes first (sorted alphabetically), null last
const named = [...map.keys()]
.filter((k): k is string => k !== null)
.sort((a, b) => a.localeCompare(b));
<Show
when={syntheses().length > 0}
fallback={
<div class="text-center py-12 bg-white rounded-lg shadow-sm border border-gray-200">
<FileText class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-medium text-gray-900">
{t('home.empty.title')}
</h3>
<p class="mt-1 text-sm text-gray-500">
{t('home.empty.description')}
</p>
<div class="mt-6">
<A
href="/generate"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
>
<Plus class="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
{t('home.empty.action')}
</A>
</div>
</div>
const groups: { name: string | null; syntheses: SynthesisListItem[] }[] = named.map(
(name) => ({ name, syntheses: map.get(name)! }),
);
if (map.has(null)) {
groups.push({ name: null, syntheses: map.get(null)! });
}
>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<For each={syntheses()}>
{(synth) => (
return groups;
});
/** Renders a single synthesis card (shared between both sort views). */
const SynthesisCard = (synth: SynthesisListItem) => (
<A
href={`/synthesis/${synth.id}`}
class="flex flex-col bg-white rounded-xl shadow-sm border border-gray-200 hover:shadow-md hover:border-indigo-300 transition-all duration-200 overflow-hidden"
>
<div class="p-6 flex-1">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2 flex-wrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
{t('home.weekLabel', { week: extractWeekNumber(synth.week) })}
</span>
<Show when={synth.theme_name}>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
{synth.theme_name}
</span>
</Show>
<Show when={!synth.theme_name && synth.theme_id === null}>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
{t('home.deletedTheme')}
</span>
</Show>
</div>
<div class="text-right">
<div class="text-sm text-gray-500">{formatDate(synth.created_at)}</div>
<div class="text-xs text-gray-400">{formatTime(synth.created_at)}</div>
@ -239,9 +216,126 @@ const Home: Component = () => {
</div>
</div>
</A>
)}
);
return (
<Show when={!loading()} fallback={<LoadingSpinner />}>
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex justify-between items-center mb-8">
<div>
<h1 class="text-3xl font-bold text-gray-900">{t('home.title')}</h1>
<p class="mt-2 text-sm text-gray-500">{t('home.subtitle')}</p>
</div>
<A
href="/generate"
class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<Plus class="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
{t('home.newSynthesis')}
</A>
</div>
{/* In-progress banner */}
<Show when={hasInProgress()}>
<div class="mb-6 bg-indigo-50 border border-indigo-200 rounded-lg p-4 flex items-center justify-between">
<div class="flex items-center">
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-indigo-600 mr-3" />
<span class="text-sm font-medium text-indigo-800">
{t('home.generationInProgress')}
</span>
</div>
<A
href="/generate"
class="text-sm font-medium text-indigo-600 hover:text-indigo-800"
>
{t('home.viewProgress')}
</A>
</div>
</Show>
{/* Error display */}
<Show when={error()}>
<div class="mb-6 bg-red-50 border border-red-200 rounded-md p-4 text-sm text-red-800">
{error()}
</div>
</Show>
<Show
when={syntheses().length > 0}
fallback={
<div class="text-center py-12 bg-white rounded-lg shadow-sm border border-gray-200">
<FileText class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-medium text-gray-900">
{t('home.empty.title')}
</h3>
<p class="mt-1 text-sm text-gray-500">
{t('home.empty.description')}
</p>
<div class="mt-6">
<A
href="/generate"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
>
<Plus class="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
{t('home.empty.action')}
</A>
</div>
</div>
}
>
{/* Sort toggle */}
<div class="flex items-center gap-3 mb-4">
<span class="text-sm text-gray-500">{t('home.sortBy')} :</span>
<button
class={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
sortBy() === 'date'
? 'bg-indigo-100 text-indigo-800'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
onClick={() => setSortBy('date')}
>
{t('home.sortDate')}
</button>
<button
class={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
sortBy() === 'theme'
? 'bg-indigo-100 text-indigo-800'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
}`}
onClick={() => setSortBy('theme')}
>
{t('home.sortTheme')}
</button>
</div>
{/* Date sort: flat grid, newest first */}
<Show when={sortBy() === 'date'}>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<For each={[...syntheses()].sort(
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
)}>
{(synth) => SynthesisCard(synth)}
</For>
</div>
</Show>
{/* Theme sort: one section per theme */}
<Show when={sortBy() === 'theme'}>
<For each={groupedByTheme()}>
{(group) => (
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-700 mb-4">
{group.name ?? t('home.deletedTheme')}
</h2>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<For each={group.syntheses}>
{(synth) => SynthesisCard(synth)}
</For>
</div>
</div>
)}
</For>
</Show>
</Show>
</div>
</Show>

Loading…
Cancel
Save