2025-12-23 02:20:57 +01:00

225 lines
6.3 KiB
TypeScript

import { useEffect, useState, type Dispatch, type SetStateAction } from 'react'
import type { Article } from '@/types/nostr'
import { useArticleEditing } from '@/hooks/useArticleEditing'
import { UserArticlesView } from './UserArticlesList'
import { EditPanel } from './UserArticlesEditPanel'
interface UserArticlesProps {
articles: Article[]
loading: boolean
error: string | null
onLoadContent: (articleId: string, authorPubkey: string) => Promise<Article | null>
showEmptyMessage?: boolean
currentPubkey: string | null
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
}
export function UserArticles({
articles,
loading,
error,
onLoadContent,
showEmptyMessage = true,
currentPubkey,
onSelectSeries,
}: UserArticlesProps) {
const controller = useUserArticlesController({ articles, onLoadContent, currentPubkey })
return (
<UserArticlesLayout
controller={controller}
loading={loading}
error={error}
showEmptyMessage={showEmptyMessage ?? true}
currentPubkey={currentPubkey}
onSelectSeries={onSelectSeries}
/>
)
}
function useUserArticlesController({
articles,
onLoadContent,
currentPubkey,
}: {
articles: Article[]
onLoadContent: (articleId: string, authorPubkey: string) => Promise<Article | null>
currentPubkey: string | null
}) {
const [localArticles, setLocalArticles] = useState<Article[]>(articles)
const [unlockedArticles, setUnlockedArticles] = useState<Set<string>>(new Set())
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null)
const editingCtx = useArticleEditing(currentPubkey)
useEffect(() => setLocalArticles(articles), [articles])
return {
localArticles,
unlockedArticles,
pendingDeleteId,
requestDelete: (id: string) => setPendingDeleteId(id),
handleUnlock: createHandleUnlock(onLoadContent, setUnlockedArticles),
handleDelete: createHandleDelete(editingCtx.deleteArticle, setLocalArticles, setPendingDeleteId),
handleEditSubmit: createHandleEditSubmit(
editingCtx.submitEdit,
editingCtx.editingDraft,
currentPubkey,
setLocalArticles
),
...editingCtx,
}
}
function createHandleUnlock(
onLoadContent: (id: string, pubkey: string) => Promise<Article | null>,
setUnlocked: Dispatch<SetStateAction<Set<string>>>
) {
return async (article: Article) => {
const full = await onLoadContent(article.id, article.pubkey)
if (full?.paid) {
setUnlocked((prev) => new Set([...prev, article.id]))
}
}
}
function createHandleDelete(
deleteArticle: (id: string) => Promise<boolean>,
setLocalArticles: Dispatch<SetStateAction<Article[]>>,
setPendingDeleteId: Dispatch<SetStateAction<string | null>>
) {
return async (article: Article) => {
const ok = await deleteArticle(article.id)
if (ok) {
setLocalArticles((prev) => prev.filter((a) => a.id !== article.id))
}
setPendingDeleteId(null)
}
}
function createHandleEditSubmit(
submitEdit: () => Promise<import('@/lib/articleMutations').ArticleUpdateResult | null>,
draft: ReturnType<typeof useArticleEditing>['editingDraft'],
currentPubkey: string | null,
setLocalArticles: Dispatch<SetStateAction<Article[]>>
) {
return async () => {
const result = await submitEdit()
if (result && draft) {
const updated = buildUpdatedArticle(draft, currentPubkey ?? '', result.articleId)
setLocalArticles((prev) => {
const filtered = prev.filter((a) => a.id !== result.originalArticleId)
return [updated, ...filtered]
})
}
}
}
function buildUpdatedArticle(
draft: NonNullable<ReturnType<typeof useArticleEditing>['editingDraft']>,
pubkey: string,
newId: string
): Article {
return {
id: newId,
pubkey,
title: draft.title,
preview: draft.preview,
content: '',
createdAt: Math.floor(Date.now() / 1000),
zapAmount: draft.zapAmount,
paid: false,
...(draft.category ? { category: draft.category } : {}),
}
}
function UserArticlesLayout({
controller,
loading,
error,
showEmptyMessage,
currentPubkey,
onSelectSeries,
}: {
controller: ReturnType<typeof useUserArticlesController>
loading: boolean
error: string | null
showEmptyMessage: boolean
currentPubkey: string | null
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
}) {
const { editPanelProps, listProps } = createLayoutProps(controller, {
loading,
error,
showEmptyMessage,
currentPubkey,
onSelectSeries,
})
return (
<div className="space-y-4">
<EditPanel {...editPanelProps} />
<UserArticlesView {...listProps} />
</div>
)
}
function createLayoutProps(
controller: ReturnType<typeof useUserArticlesController>,
view: {
loading: boolean
error: string | null
showEmptyMessage: boolean
currentPubkey: string | null
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
}
) {
return {
editPanelProps: buildEditPanelProps(controller),
listProps: buildListProps(controller, view),
}
}
function buildEditPanelProps(controller: ReturnType<typeof useUserArticlesController>) {
return {
draft: controller.editingDraft,
editingArticleId: controller.editingArticleId,
loading: controller.loading,
error: controller.error,
onCancel: controller.cancelEditing,
onDraftChange: controller.updateDraft,
onSubmit: controller.handleEditSubmit,
}
}
function buildListProps(
controller: ReturnType<typeof useUserArticlesController>,
view: {
loading: boolean
error: string | null
showEmptyMessage: boolean
currentPubkey: string | null
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
}
) {
return {
articles: controller.localArticles,
loading: view.loading,
error: view.error,
showEmptyMessage: view.showEmptyMessage,
unlockedArticles: controller.unlockedArticles,
onUnlock: (a: Article) => {
void controller.handleUnlock(a)
},
onEdit: (a: Article) => {
void controller.startEditing(a)
},
onDelete: (a: Article) => {
void controller.handleDelete(a)
},
editingArticleId: controller.editingArticleId,
currentPubkey: view.currentPubkey,
pendingDeleteId: controller.pendingDeleteId,
requestDelete: controller.requestDelete,
...(view.onSelectSeries ? { onSelectSeries: view.onSelectSeries } : {}),
}
}