200 lines
5.5 KiB
TypeScript
200 lines
5.5 KiB
TypeScript
import { ArticleCard } from './ArticleCard'
|
|
import type { Article } from '@/types/nostr'
|
|
import { memo } from 'react'
|
|
import Link from 'next/link'
|
|
|
|
interface UserArticlesViewProps {
|
|
articles: Article[]
|
|
loading: boolean
|
|
error: string | null
|
|
showEmptyMessage?: boolean
|
|
unlockedArticles: Set<string>
|
|
onUnlock: (article: Article) => void
|
|
onEdit: (article: Article) => void
|
|
onDelete: (article: Article) => void
|
|
editingArticleId: string | null
|
|
currentPubkey: string | null
|
|
pendingDeleteId: string | null
|
|
requestDelete: (articleId: string) => void
|
|
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
|
|
}
|
|
|
|
const ArticlesLoading = () => (
|
|
<div className="text-center py-12">
|
|
<p className="text-gray-500">Loading articles...</p>
|
|
</div>
|
|
)
|
|
|
|
const ArticlesError = ({ message }: { message: string }) => (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
|
<p className="text-red-800">{message}</p>
|
|
</div>
|
|
)
|
|
|
|
const EmptyState = ({ show }: { show: boolean }) =>
|
|
show ? (
|
|
<div className="text-center py-12">
|
|
<p className="text-gray-500">No articles published yet.</p>
|
|
</div>
|
|
) : null
|
|
|
|
function ArticleActions({
|
|
article,
|
|
onEdit,
|
|
onDelete,
|
|
editingArticleId,
|
|
pendingDeleteId,
|
|
requestDelete,
|
|
}: {
|
|
article: Article
|
|
onEdit: (article: Article) => void
|
|
onDelete: (article: Article) => void
|
|
editingArticleId: string | null
|
|
pendingDeleteId: string | null
|
|
requestDelete: (articleId: string) => void
|
|
}) {
|
|
return (
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => onEdit(article)}
|
|
className="px-3 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
|
|
disabled={editingArticleId !== null && editingArticleId !== article.id}
|
|
>
|
|
Edit
|
|
</button>
|
|
<button
|
|
onClick={() => (pendingDeleteId === article.id ? onDelete(article) : requestDelete(article.id))}
|
|
className="px-3 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700"
|
|
>
|
|
{pendingDeleteId === article.id ? 'Confirm delete' : 'Delete'}
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ArticleRow(
|
|
props: Omit<UserArticlesViewProps, 'articles' | 'loading' | 'error' | 'showEmptyMessage'> & {
|
|
article: Article
|
|
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
|
|
}
|
|
) {
|
|
const content = buildArticleContent(props)
|
|
return <div className="space-y-3">{content}</div>
|
|
}
|
|
|
|
function buildArticleContent(
|
|
props: Omit<UserArticlesViewProps, 'articles' | 'loading' | 'error' | 'showEmptyMessage'> & {
|
|
article: Article
|
|
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
|
|
}
|
|
) {
|
|
const parts = [buildArticleCard(props), buildSeriesLink(props), buildActions(props)].filter(Boolean)
|
|
return parts as JSX.Element[]
|
|
}
|
|
|
|
function buildArticleCard(
|
|
props: Omit<UserArticlesViewProps, 'articles' | 'loading' | 'error' | 'showEmptyMessage'> & { article: Article }
|
|
) {
|
|
const { article, unlockedArticles, onUnlock } = props
|
|
return (
|
|
<ArticleCard
|
|
key="card"
|
|
article={{ ...article, paid: unlockedArticles.has(article.id) || article.paid }}
|
|
onUnlock={onUnlock}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function buildSeriesLink(
|
|
props: Omit<UserArticlesViewProps, 'articles' | 'loading' | 'error' | 'showEmptyMessage'> & {
|
|
article: Article
|
|
onSelectSeries?: ((seriesId: string | undefined) => void) | undefined
|
|
}
|
|
) {
|
|
const { article, onSelectSeries } = props
|
|
if (!article.seriesId) {
|
|
return null
|
|
}
|
|
return (
|
|
<div key="series" className="text-xs text-blue-700 flex gap-2 items-center">
|
|
<span>Série :</span>
|
|
<Link href={`/series/${article.seriesId}`} className="underline">
|
|
Ouvrir
|
|
</Link>
|
|
{onSelectSeries && (
|
|
<button type="button" className="underline" onClick={() => onSelectSeries(article.seriesId)}>
|
|
Filtrer
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function buildActions(
|
|
props: Omit<UserArticlesViewProps, 'articles' | 'loading' | 'error' | 'showEmptyMessage'> & { article: Article }
|
|
) {
|
|
const { article, currentPubkey, onEdit, onDelete, editingArticleId, pendingDeleteId, requestDelete } = props
|
|
if (currentPubkey !== article.pubkey) {
|
|
return null
|
|
}
|
|
return (
|
|
<ArticleActions
|
|
key="actions"
|
|
article={article}
|
|
onEdit={onEdit}
|
|
onDelete={onDelete}
|
|
editingArticleId={editingArticleId}
|
|
pendingDeleteId={pendingDeleteId}
|
|
requestDelete={requestDelete}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function UserArticlesViewComponent(props: UserArticlesViewProps) {
|
|
if (props.loading) {
|
|
return <ArticlesLoading />
|
|
}
|
|
if (props.error) {
|
|
return <ArticlesError message={props.error} />
|
|
}
|
|
if ((props.showEmptyMessage ?? true) && props.articles.length === 0) {
|
|
return <EmptyState show />
|
|
}
|
|
return renderArticles(props)
|
|
}
|
|
|
|
function renderArticles({
|
|
articles,
|
|
unlockedArticles,
|
|
onUnlock,
|
|
onEdit,
|
|
onDelete,
|
|
editingArticleId,
|
|
currentPubkey,
|
|
pendingDeleteId,
|
|
requestDelete,
|
|
onSelectSeries,
|
|
}: UserArticlesViewProps) {
|
|
return (
|
|
<div className="space-y-6">
|
|
{articles.map((article) => (
|
|
<ArticleRow
|
|
key={article.id}
|
|
article={article}
|
|
unlockedArticles={unlockedArticles}
|
|
onUnlock={onUnlock}
|
|
onEdit={onEdit}
|
|
onDelete={onDelete}
|
|
editingArticleId={editingArticleId}
|
|
currentPubkey={currentPubkey}
|
|
pendingDeleteId={pendingDeleteId}
|
|
requestDelete={requestDelete}
|
|
onSelectSeries={onSelectSeries}
|
|
/>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const UserArticlesView = memo(UserArticlesViewComponent)
|