138 lines
3.8 KiB
TypeScript
138 lines
3.8 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react'
|
|
import type { Series } from '@/types/nostr'
|
|
import { SeriesList } from './SeriesList'
|
|
import { SeriesStats } from './SeriesStats'
|
|
import { getSeriesByAuthor } from '@/lib/seriesQueries'
|
|
import { getSeriesAggregates } from '@/lib/seriesAggregation'
|
|
|
|
interface SeriesSectionProps {
|
|
authorPubkey: string
|
|
onSelect: (seriesId: string | undefined) => void
|
|
selectedId?: string | undefined
|
|
}
|
|
|
|
export function SeriesSection({ authorPubkey, onSelect, selectedId }: SeriesSectionProps) {
|
|
const [{ series, loading, error, aggregates }, load] = useSeriesData(authorPubkey)
|
|
|
|
if (loading) {
|
|
return <p className="text-sm text-gray-600">Chargement des séries...</p>
|
|
}
|
|
if (error) {
|
|
return <p className="text-sm text-red-600">{error}</p>
|
|
}
|
|
return (
|
|
<div className="space-y-4">
|
|
<SeriesControls onSelect={onSelect} onReload={load} />
|
|
<SeriesList
|
|
series={series}
|
|
onSelect={onSelect}
|
|
{...(selectedId ? { selectedId } : {})}
|
|
/>
|
|
<SeriesAggregatesList series={series} aggregates={aggregates} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SeriesControls({
|
|
onSelect,
|
|
onReload,
|
|
}: {
|
|
onSelect: (id: string | undefined) => void
|
|
onReload: () => Promise<void>
|
|
}) {
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
className="px-3 py-1 text-sm rounded bg-gray-200"
|
|
onClick={() => onSelect(undefined)}
|
|
>
|
|
Toutes les séries
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="text-xs text-blue-600 underline"
|
|
onClick={() => {
|
|
void onReload()
|
|
}}
|
|
>
|
|
Recharger
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SeriesAggregatesList({
|
|
series,
|
|
aggregates,
|
|
}: {
|
|
series: Series[]
|
|
aggregates: Record<string, { sponsoring: number; purchases: number; reviewTips: number }>
|
|
}) {
|
|
return (
|
|
<>
|
|
{series.map((s) => {
|
|
const agg = aggregates[s.id] ?? { sponsoring: 0, purchases: 0, reviewTips: 0 }
|
|
return (
|
|
<div key={s.id} className="mt-2">
|
|
<SeriesStats sponsoring={agg.sponsoring} purchases={agg.purchases} reviewTips={agg.reviewTips} />
|
|
</div>
|
|
)
|
|
})}
|
|
</>
|
|
)
|
|
}
|
|
|
|
function useSeriesData(authorPubkey: string): [
|
|
{
|
|
series: Series[]
|
|
loading: boolean
|
|
error: string | null
|
|
aggregates: Record<string, { sponsoring: number; purchases: number; reviewTips: number }>
|
|
},
|
|
() => Promise<void>
|
|
] {
|
|
const [series, setSeries] = useState<Series[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [aggregates, setAggregates] = useState<Record<string, { sponsoring: number; purchases: number; reviewTips: number }>>({})
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const { items, aggregates: agg } = await fetchSeriesAndAggregates(authorPubkey)
|
|
setSeries(items)
|
|
setAggregates(agg)
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Erreur lors du chargement des séries')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [authorPubkey])
|
|
|
|
useEffect(() => {
|
|
void load()
|
|
}, [load])
|
|
|
|
return [{ series, loading, error, aggregates }, load]
|
|
}
|
|
|
|
async function fetchSeriesAndAggregates(authorPubkey: string): Promise<{
|
|
items: Series[]
|
|
aggregates: Record<string, { sponsoring: number; purchases: number; reviewTips: number }>
|
|
}> {
|
|
const items = await getSeriesByAuthor(authorPubkey)
|
|
const aggEntries = await Promise.all(
|
|
items.map(async (s) => {
|
|
const agg = await getSeriesAggregates({ authorPubkey, seriesId: s.id })
|
|
return [s.id, agg] as const
|
|
})
|
|
)
|
|
const aggMap: Record<string, { sponsoring: number; purchases: number; reviewTips: number }> = {}
|
|
aggEntries.forEach(([id, agg]) => {
|
|
aggMap[id] = agg
|
|
})
|
|
return { items, aggregates: aggMap }
|
|
}
|