feat: navigate drafts by subject instead of loading all at once
This commit is contained in:
parent
c507648f14
commit
41957b389a
6 changed files with 152 additions and 90 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue