225 lines
6.3 KiB
TypeScript
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 } : {}),
|
|
}
|
|
}
|