feat: navigate drafts by subject instead of loading all at once

This commit is contained in:
Jalil Arfaoui 2026-04-01 00:52:02 +02:00
parent c507648f14
commit 41957b389a
6 changed files with 152 additions and 90 deletions

View file

@ -1,38 +1,3 @@
.filter {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 24px;
}
.label {
font-family: var(--font-gotham-bold);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-light);
}
.select {
padding: 8px 12px;
border: 1px solid var(--border-light);
border-radius: 4px;
font-family: var(--font-gotham-book);
font-size: 14px;
flex: 1;
}
.select:focus {
outline: none;
border-color: var(--debats-red);
}
.empty {
font-family: var(--font-gotham-book);
font-size: 15px;
color: var(--text-light);
}
.list {
display: flex;
flex-direction: column;

View file

@ -1,6 +1,3 @@
'use client'
import { useMemo, useState } from 'react'
import { DraftStatement } from '../../../../domain/entities/draft-statement'
import { DraftResolution } from '../../../../domain/use-cases/resolve-draft'
import DraftCard from '../DraftCard'
@ -16,54 +13,11 @@ interface DraftListProps {
}
export default function DraftList({ drafts }: DraftListProps) {
const [selectedSubject, setSelectedSubject] = useState('')
const { subjectTitles, countBySubject } = useMemo(() => {
const counts = new Map<string, number>()
for (const d of drafts) {
const t = d.draft.subjectTitle
counts.set(t, (counts.get(t) ?? 0) + 1)
}
const titles = Array.from(counts.keys()).sort((a, b) => a.localeCompare(b, 'fr'))
return { subjectTitles: titles, countBySubject: counts }
}, [drafts])
const filtered = selectedSubject
? drafts.filter((d) => d.draft.subjectTitle === selectedSubject)
: drafts
return (
<>
{subjectTitles.length > 1 && (
<div className={styles.filter}>
<label className={styles.label} htmlFor="subject-filter">
Sujet
</label>
<select
id="subject-filter"
className={styles.select}
value={selectedSubject}
onChange={(e) => setSelectedSubject(e.target.value)}
>
<option value="">Tous les sujets ({drafts.length})</option>
{subjectTitles.map((title) => (
<option key={title} value={title}>
{title} ({countBySubject.get(title)})
</option>
))}
</select>
</div>
)}
{filtered.length === 0 ? (
<p className={styles.empty}>Aucun brouillon pour ce sujet.</p>
) : (
<div className={styles.list}>
{filtered.map(({ draft, resolution }) => (
<DraftCard key={draft.id} draft={draft} resolution={resolution} />
))}
</div>
)}
</>
<div className={styles.list}>
{drafts.map(({ draft, resolution }) => (
<DraftCard key={draft.id} draft={draft} resolution={resolution} />
))}
</div>
)
}

View file

@ -24,3 +24,61 @@
font-size: 15px;
color: var(--text-light);
}
.total {
font-family: var(--font-gotham-book);
font-size: 15px;
color: var(--text-light);
margin: 0 0 20px 0;
}
.subjectList {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.subjectLink {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
text-decoration: none;
color: var(--text-dark);
font-family: var(--font-gotham-book);
font-size: 15px;
border-radius: 4px;
transition: background-color 0.1s ease;
}
.subjectLink:hover {
background-color: #f5f3ef;
}
.subjectCount {
font-family: var(--font-gotham-bold);
font-size: 13px;
color: white;
background-color: var(--debats-red);
border-radius: 12px;
padding: 2px 10px;
min-width: 24px;
text-align: center;
}
.backLink {
display: inline-block;
font-family: var(--font-gotham-book);
font-size: 14px;
color: var(--text-light);
text-decoration: none;
margin-bottom: 16px;
transition: color 0.15s ease;
}
.backLink:hover {
color: var(--debats-red);
}

View file

@ -1,4 +1,5 @@
import { Metadata } from 'next'
import Link from 'next/link'
import { Effect } from 'effect'
import { createAdminSupabaseClient } from '../../../infra/supabase/admin'
import { createDraftStatementRepository } from '../../../infra/database/draft-statement-repository-supabase'
@ -15,7 +16,11 @@ export const metadata: Metadata = {
robots: { index: false, follow: false },
}
export default async function AdminDraftsPage() {
interface Props {
searchParams: Promise<{ subject?: string }>
}
export default async function AdminDraftsPage({ searchParams }: Props) {
const contributor = await getAdminContributor()
if (!contributor) {
return (
@ -27,11 +32,47 @@ export default async function AdminDraftsPage() {
const supabase = createAdminSupabaseClient()
const draftRepo = createDraftStatementRepository(supabase)
const { subject } = await searchParams
if (!subject) {
const subjectCounts = await Effect.runPromise(draftRepo.countPendingBySubject())
const total = subjectCounts.reduce((sum, s) => sum + s.count, 0)
return (
<div className={styles.container}>
<h1 className={styles.title}>BROUILLONS EN ATTENTE</h1>
{subjectCounts.length === 0 ? (
<p className={styles.empty}>Aucun brouillon en attente de validation.</p>
) : (
<>
<p className={styles.total}>
{total} brouillon{total > 1 ? 's' : ''} en attente
</p>
<ul className={styles.subjectList}>
{subjectCounts.map(({ subjectTitle, count }) => (
<li key={subjectTitle}>
<Link
href={`/admin/drafts?subject=${encodeURIComponent(subjectTitle)}`}
className={styles.subjectLink}
>
{subjectTitle}
<span className={styles.subjectCount}>{count}</span>
</Link>
</li>
))}
</ul>
</>
)}
</div>
)
}
const publicFigureRepo = createPublicFigureRepository(supabase)
const subjectRepo = createSubjectRepository(supabase)
const positionRepo = createPositionRepository(supabase)
const drafts = await Effect.runPromise(draftRepo.findByStatus('pending'))
const drafts = await Effect.runPromise(draftRepo.findPendingBySubject(subject))
const draftsWithResolution = await Promise.all(
drafts.map(async (draft) => {
@ -44,10 +85,13 @@ export default async function AdminDraftsPage() {
return (
<div className={styles.container}>
<h1 className={styles.title}>BROUILLONS EN ATTENTE</h1>
<Link href="/admin/drafts" className={styles.backLink}>
Tous les sujets
</Link>
<h1 className={styles.title}>{subject}</h1>
{draftsWithResolution.length === 0 ? (
<p className={styles.empty}>Aucun brouillon en attente de validation.</p>
<p className={styles.empty}>Aucun brouillon en attente pour ce sujet.</p>
) : (
<DraftList drafts={draftsWithResolution} />
)}

View file

@ -2,8 +2,12 @@ import { Effect } from 'effect'
import { DraftStatement } from '../entities/draft-statement'
import { DatabaseError } from './errors'
export type SubjectDraftCount = { subjectTitle: string; count: number }
export interface DraftStatementRepository {
findByStatus(status: DraftStatement['status']): Effect.Effect<DraftStatement[], DatabaseError>
findPendingBySubject(subjectTitle: string): Effect.Effect<DraftStatement[], DatabaseError>
countPendingBySubject(): Effect.Effect<SubjectDraftCount[], DatabaseError>
findById(id: string): Effect.Effect<DraftStatement | null, DatabaseError>
update(
id: string,

View file

@ -51,6 +51,43 @@ export function createDraftStatementRepository(supabase: SupabaseClient): DraftS
catch: (error) => dbError(`Failed to fetch ${status} drafts`, error),
}),
findPendingBySubject: (subjectTitle: string) =>
Effect.tryPromise({
try: async () => {
const { data, error } = await supabase
.from('draft_statements')
.select('*')
.eq('status', 'pending')
.eq('subject_title', subjectTitle)
.order('created_at', { ascending: false })
if (error) throw error
return data.map(mapRow)
},
catch: (error) => dbError('Failed to fetch pending drafts by subject', error),
}),
countPendingBySubject: () =>
Effect.tryPromise({
try: async () => {
const { data, error } = await supabase
.from('draft_statements')
.select('subject_title')
.eq('status', 'pending')
if (error) throw error
const counts = new Map<string, number>()
for (const row of data) {
const title = row.subject_title as string
counts.set(title, (counts.get(title) ?? 0) + 1)
}
return Array.from(counts, ([subjectTitle, count]) => ({ subjectTitle, count })).sort(
(a, b) => b.count - a.count,
)
},
catch: (error) => dbError('Failed to count pending drafts by subject', error),
}),
findById: (id: string) =>
Effect.tryPromise({
try: async () => {