130 lines
4.4 KiB
TypeScript
130 lines
4.4 KiB
TypeScript
import { useState } from 'react'
|
|
import Head from 'next/head'
|
|
import { ConnectButton } from '@/components/ConnectButton'
|
|
import { ArticleCard } from '@/components/ArticleCard'
|
|
import { SearchBar } from '@/components/SearchBar'
|
|
import { ArticleFiltersComponent, type ArticleFilters } from '@/components/ArticleFilters'
|
|
import { useArticles } from '@/hooks/useArticles'
|
|
import type { Article } from '@/types/nostr'
|
|
|
|
export default function Home() {
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
const [filters, setFilters] = useState<ArticleFilters>({
|
|
authorPubkey: null,
|
|
minPrice: null,
|
|
maxPrice: null,
|
|
sortBy: 'newest',
|
|
})
|
|
|
|
const { articles, allArticles, loading, error, loadArticleContent } = useArticles(
|
|
searchQuery,
|
|
filters
|
|
)
|
|
const [unlockedArticles, setUnlockedArticles] = useState<Set<string>>(new Set())
|
|
|
|
const handleUnlock = async (article: Article) => {
|
|
const fullArticle = await loadArticleContent(article.id, article.pubkey)
|
|
if (fullArticle && fullArticle.paid) {
|
|
setUnlockedArticles((prev) => new Set([...prev, article.id]))
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Head>
|
|
<title>Nostr Paywall - Articles with Lightning Payments</title>
|
|
<meta name="description" content="Read article previews for free, unlock full content with Lightning zaps" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<link rel="icon" href="/favicon.ico" />
|
|
</Head>
|
|
|
|
<main className="min-h-screen bg-gray-50">
|
|
<header className="bg-white shadow-sm">
|
|
<div className="max-w-4xl mx-auto px-4 py-4 flex justify-between items-center">
|
|
<h1 className="text-2xl font-bold text-gray-900">Nostr Paywall</h1>
|
|
<div className="flex items-center gap-4">
|
|
<a
|
|
href="/docs"
|
|
className="px-4 py-2 text-gray-600 hover:text-gray-800 text-sm font-medium transition-colors"
|
|
>
|
|
Documentation
|
|
</a>
|
|
<a
|
|
href="/publish"
|
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors"
|
|
>
|
|
Publish Article
|
|
</a>
|
|
<ConnectButton />
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="max-w-4xl mx-auto px-4 py-8">
|
|
<div className="mb-8">
|
|
<h2 className="text-3xl font-bold mb-4">Articles</h2>
|
|
<p className="text-gray-600 mb-4">
|
|
Read previews for free, unlock full content with {800} sats Lightning zaps
|
|
</p>
|
|
|
|
{/* Search Bar */}
|
|
<div className="mb-4">
|
|
<SearchBar value={searchQuery} onChange={setSearchQuery} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
{!loading && allArticles.length > 0 && (
|
|
<ArticleFiltersComponent
|
|
filters={filters}
|
|
onFiltersChange={setFilters}
|
|
articles={allArticles}
|
|
/>
|
|
)}
|
|
|
|
{loading && (
|
|
<div className="text-center py-12">
|
|
<p className="text-gray-500">Loading articles...</p>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
|
<p className="text-red-800">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && articles.length === 0 && (
|
|
<div className="text-center py-12">
|
|
<p className="text-gray-500">
|
|
{allArticles.length === 0
|
|
? 'No articles found. Check back later!'
|
|
: 'No articles match your search or filters.'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && articles.length > 0 && (
|
|
<div className="mb-4 text-sm text-gray-600">
|
|
Showing {articles.length} of {allArticles.length} article{allArticles.length !== 1 ? 's' : ''}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-6">
|
|
{articles.map((article) => (
|
|
<ArticleCard
|
|
key={article.id}
|
|
article={{
|
|
...article,
|
|
paid: unlockedArticles.has(article.id) || article.paid,
|
|
}}
|
|
onUnlock={handleUnlock}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</>
|
|
)
|
|
}
|