diff --git a/components/AuthorCard.tsx b/components/AuthorCard.tsx
new file mode 100644
index 0000000..4ad6cf4
--- /dev/null
+++ b/components/AuthorCard.tsx
@@ -0,0 +1,42 @@
+import Link from 'next/link'
+import Image from 'next/image'
+import type { Article } from '@/types/nostr'
+import { t } from '@/lib/i18n'
+
+interface AuthorCardProps {
+ presentation: Article
+}
+
+export function AuthorCard({ presentation }: AuthorCardProps) {
+ const authorName = presentation.title.replace(/^Présentation de /, '') || 'Auteur'
+ const totalBTC = (presentation.totalSponsoring ?? 0) / 100_000_000
+
+ return (
+
+
+ {presentation.bannerUrl && (
+
+
+
+ )}
+
+
{authorName}
+
{presentation.preview}
+ {presentation.totalSponsoring !== undefined && presentation.totalSponsoring > 0 && (
+
+ {t('author.sponsoring.total', { amount: totalBTC.toFixed(6) })} BTC
+
+ )}
+
+
+
+ )
+}
diff --git a/components/AuthorPresentationEditor.tsx b/components/AuthorPresentationEditor.tsx
index ce77d8d..31ced75 100644
--- a/components/AuthorPresentationEditor.tsx
+++ b/components/AuthorPresentationEditor.tsx
@@ -20,13 +20,23 @@ interface AuthorPresentationDraft {
const ADDRESS_PATTERN = /^(1|3|bc1)[a-zA-Z0-9]{25,62}$/
-function SuccessNotice() {
+function SuccessNotice({ pubkey }: { pubkey: string | null }) {
return (
{t('presentation.success')}
-
+
{t('presentation.successMessage')}
+ {pubkey && (
+
+ )}
)
}
@@ -344,7 +354,7 @@ function AuthorPresentationFormView({
return
}
if (state.success) {
- return
+ return
}
return (
diff --git a/components/AuthorsList.tsx b/components/AuthorsList.tsx
new file mode 100644
index 0000000..78f6335
--- /dev/null
+++ b/components/AuthorsList.tsx
@@ -0,0 +1,60 @@
+import type { Article } from '@/types/nostr'
+import { AuthorCard } from './AuthorCard'
+
+interface AuthorsListProps {
+ authors: Article[]
+ allAuthors: Article[]
+ loading: boolean
+ error: string | null
+}
+
+function LoadingState() {
+ return (
+
+ )
+}
+
+function ErrorState({ message }: { message: string }) {
+ return (
+
+ )
+}
+
+function EmptyState({ hasAny }: { hasAny: boolean }) {
+ return (
+
+
+ {hasAny ? 'No authors match your search or filters.' : 'No authors found. Check back later!'}
+
+
+ )
+}
+
+export function AuthorsList({ authors, allAuthors, loading, error }: AuthorsListProps) {
+ if (loading) {
+ return
+ }
+ if (error) {
+ return
+ }
+ if (authors.length === 0) {
+ return 0} />
+ }
+
+ return (
+ <>
+
+ Showing {authors.length} of {allAuthors.length} author{allAuthors.length !== 1 ? 's' : ''}
+
+
+ {authors.map((author) => (
+
+ ))}
+
+ >
+ )
+}
diff --git a/components/HomeView.tsx b/components/HomeView.tsx
index 5f861bf..01c8ac8 100644
--- a/components/HomeView.tsx
+++ b/components/HomeView.tsx
@@ -4,6 +4,7 @@ import { ArticleFiltersComponent, type ArticleFilters } from '@/components/Artic
import { CategoryTabs } from '@/components/CategoryTabs'
import { SearchBar } from '@/components/SearchBar'
import { ArticlesList } from '@/components/ArticlesList'
+import { AuthorsList } from '@/components/AuthorsList'
import { PageHeader } from '@/components/PageHeader'
import { Footer } from '@/components/Footer'
import { FundingGauge } from '@/components/FundingGauge'
@@ -18,6 +19,8 @@ interface HomeViewProps {
setFilters: Dispatch>
articles: Article[]
allArticles: Article[]
+ authors: Article[]
+ allAuthors: Article[]
loading: boolean
error: string | null
onUnlock: (article: Article) => void
@@ -85,13 +88,17 @@ function HomeContent({
setFilters,
articles,
allArticles,
+ authors,
+ allAuthors,
loading,
error,
onUnlock,
unlockedArticles,
}: HomeViewProps) {
const shouldShowFilters = !loading && allArticles.length > 0
+ const shouldShowAuthors = selectedCategory !== null && selectedCategory !== 'all'
const articlesListProps = { articles, allArticles, loading, error, onUnlock, unlockedArticles }
+ const authorsListProps = { authors, allAuthors, loading, error }
return (
@@ -102,11 +109,15 @@ function HomeContent({
setSelectedCategory={setSelectedCategory}
/>
- {shouldShowFilters && (
+ {shouldShowFilters && !shouldShowAuthors && (
)}
-
+ {shouldShowAuthors ? (
+
+ ) : (
+
+ )}
diff --git a/lib/articlePublisherHelpersPresentation.ts b/lib/articlePublisherHelpersPresentation.ts
index 6ef7dbf..f7ed475 100644
--- a/lib/articlePublisherHelpersPresentation.ts
+++ b/lib/articlePublisherHelpersPresentation.ts
@@ -31,6 +31,9 @@ export function parsePresentationEvent(event: Event): import('@/types/nostr').Au
return null
}
+ // Map tag category to article category
+ const articleCategory = tags.category === 'sciencefiction' ? 'science-fiction' : tags.category === 'research' ? 'scientific-research' : undefined
+
return {
id: tags.id ?? event.id,
pubkey: event.pubkey,
@@ -44,6 +47,7 @@ export function parsePresentationEvent(event: Event): import('@/types/nostr').Au
isPresentation: true,
mainnetAddress: tags.mainnetAddress ?? '',
totalSponsoring: tags.totalSponsoring ?? 0,
+ originalCategory: articleCategory, // Store original category for filtering
...(tags.pictureUrl !== undefined && tags.pictureUrl !== null && typeof tags.pictureUrl === 'string' ? { bannerUrl: tags.pictureUrl } : {}),
}
}
diff --git a/lib/authorFiltering.ts b/lib/authorFiltering.ts
new file mode 100644
index 0000000..9fca279
--- /dev/null
+++ b/lib/authorFiltering.ts
@@ -0,0 +1,63 @@
+import type { Article } from '@/types/nostr'
+import type { ArticleFilters } from '@/components/ArticleFilters'
+
+/**
+ * Map category from ArticleFilters to tag category
+ */
+function mapCategoryToTag(category: ArticleFilters['category']): 'sciencefiction' | 'research' | null {
+ if (category === 'science-fiction') {
+ return 'sciencefiction'
+ }
+ if (category === 'scientific-research') {
+ return 'research'
+ }
+ return null
+}
+
+/**
+ * Get authors (presentation articles) filtered by category
+ */
+export function getAuthorsByCategory(
+ presentationArticles: Map,
+ category: ArticleFilters['category']
+): Article[] {
+ const authors: Article[] = []
+
+ presentationArticles.forEach((presentation) => {
+ if (!presentation.isPresentation) {
+ return
+ }
+
+ // If no category filter, include all authors
+ if (!category || category === 'all') {
+ authors.push(presentation)
+ return
+ }
+
+ // Check if presentation matches category using originalCategory
+ const presentationWithCategory = presentation as Article & { originalCategory?: 'science-fiction' | 'scientific-research' }
+ if (presentationWithCategory.originalCategory === category) {
+ authors.push(presentation)
+ }
+ })
+
+ return authors
+}
+
+/**
+ * Sort authors by sponsoring (descending) then by date (newest first)
+ */
+export function sortAuthors(authors: Article[]): Article[] {
+ return [...authors].sort((a, b) => {
+ const sponsoringA = a.totalSponsoring ?? 0
+ const sponsoringB = b.totalSponsoring ?? 0
+
+ // First sort by sponsoring (descending)
+ if (sponsoringA !== sponsoringB) {
+ return sponsoringB - sponsoringA
+ }
+
+ // Then sort by date (newest first)
+ return b.createdAt - a.createdAt
+ })
+}
diff --git a/locales/en.txt b/locales/en.txt
index 49879d6..29f1dca 100644
--- a/locales/en.txt
+++ b/locales/en.txt
@@ -59,6 +59,7 @@ presentation.title=Create your presentation article
presentation.description=This article is required to publish on zapwall.fr. It allows readers to know you and sponsor you.
presentation.success=Presentation article created!
presentation.successMessage=Your presentation article has been created successfully. You can now publish articles.
+presentation.manageSeries=Manage my series
presentation.profileNote=This profile data is specific to zapwall.fr and may differ from your Nostr profile.
presentation.field.picture=Profile picture
presentation.field.picture.help=Profile image for your author page (max 5MB, formats: PNG, JPG, WebP)
diff --git a/locales/fr.txt b/locales/fr.txt
index ae4fc40..200e813 100644
--- a/locales/fr.txt
+++ b/locales/fr.txt
@@ -59,6 +59,7 @@ presentation.title=Créer votre article de présentation
presentation.description=Cet article est obligatoire pour publier sur zapwall.fr. Il permet aux lecteurs de vous connaître et de vous sponsoriser.
presentation.success=Article de présentation créé !
presentation.successMessage=Votre article de présentation a été créé avec succès. Vous pouvez maintenant publier des articles.
+presentation.manageSeries=Gérer mes séries
presentation.profileNote=Les données de ce profil sont spécifiques à zapwall.fr et peuvent différer de votre profil Nostr.
presentation.field.picture=Photo de profil
presentation.field.picture.help=Image de profil pour votre page auteur (max 5Mo, formats: PNG, JPG, WebP)
diff --git a/pages/index.tsx b/pages/index.tsx
index b1ccc59..feb7f87 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react'
import { useArticles } from '@/hooks/useArticles'
import { useNostrAuth } from '@/hooks/useNostrAuth'
import { applyFiltersAndSort } from '@/lib/articleFiltering'
+import { getAuthorsByCategory, sortAuthors } from '@/lib/authorFiltering'
import type { Article } from '@/types/nostr'
import type { ArticleFilters } from '@/components/ArticleFilters'
import { HomeView } from '@/components/HomeView'
@@ -104,6 +105,16 @@ function useHomeController() {
const articles = useFilteredArticles(allArticlesRaw, searchQuery, filters, presentationArticles)
const handleUnlock = useUnlockHandler(loadArticleContent, setUnlockedArticles)
+ // Get authors by category
+ const allAuthors = useMemo(() => {
+ const authorsArray = Array.from(presentationArticles.values())
+ return sortAuthors(authorsArray)
+ }, [presentationArticles])
+
+ const authors = useMemo(() => {
+ return getAuthorsByCategory(presentationArticles, selectedCategory)
+ }, [presentationArticles, selectedCategory])
+
return {
searchQuery,
setSearchQuery,
@@ -113,6 +124,8 @@ function useHomeController() {
setFilters,
articles,
allArticles,
+ authors,
+ allAuthors,
loading,
error,
unlockedArticles,
diff --git a/public/locales/en.txt b/public/locales/en.txt
index 49879d6..29f1dca 100644
--- a/public/locales/en.txt
+++ b/public/locales/en.txt
@@ -59,6 +59,7 @@ presentation.title=Create your presentation article
presentation.description=This article is required to publish on zapwall.fr. It allows readers to know you and sponsor you.
presentation.success=Presentation article created!
presentation.successMessage=Your presentation article has been created successfully. You can now publish articles.
+presentation.manageSeries=Manage my series
presentation.profileNote=This profile data is specific to zapwall.fr and may differ from your Nostr profile.
presentation.field.picture=Profile picture
presentation.field.picture.help=Profile image for your author page (max 5MB, formats: PNG, JPG, WebP)
diff --git a/public/locales/fr.txt b/public/locales/fr.txt
index ae4fc40..200e813 100644
--- a/public/locales/fr.txt
+++ b/public/locales/fr.txt
@@ -59,6 +59,7 @@ presentation.title=Créer votre article de présentation
presentation.description=Cet article est obligatoire pour publier sur zapwall.fr. Il permet aux lecteurs de vous connaître et de vous sponsoriser.
presentation.success=Article de présentation créé !
presentation.successMessage=Votre article de présentation a été créé avec succès. Vous pouvez maintenant publier des articles.
+presentation.manageSeries=Gérer mes séries
presentation.profileNote=Les données de ce profil sont spécifiques à zapwall.fr et peuvent différer de votre profil Nostr.
presentation.field.picture=Photo de profil
presentation.field.picture.help=Image de profil pour votre page auteur (max 5Mo, formats: PNG, JPG, WebP)
diff --git a/types/nostr.ts b/types/nostr.ts
index 86d40ec..46c96e4 100644
--- a/types/nostr.ts
+++ b/types/nostr.ts
@@ -52,6 +52,7 @@ export interface AuthorPresentationArticle extends Article {
isPresentation: true
mainnetAddress: string
totalSponsoring: number
+ originalCategory?: 'science-fiction' | 'scientific-research' // Original category from tags for filtering
}
export interface Series {