From 4787bd541088968db7f6934bfcf0b174a47f2110 Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Mon, 5 Jan 2026 23:24:10 +0100 Subject: [PATCH] Fix: profil image2 --- components/AuthorCard.tsx | 42 +++++++++++++++ components/AuthorPresentationEditor.tsx | 16 ++++-- components/AuthorsList.tsx | 60 +++++++++++++++++++++ components/HomeView.tsx | 15 +++++- lib/articlePublisherHelpersPresentation.ts | 4 ++ lib/authorFiltering.ts | 63 ++++++++++++++++++++++ locales/en.txt | 1 + locales/fr.txt | 1 + pages/index.tsx | 13 +++++ public/locales/en.txt | 1 + public/locales/fr.txt | 1 + types/nostr.ts | 1 + 12 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 components/AuthorCard.tsx create mode 100644 components/AuthorsList.tsx create mode 100644 lib/authorFiltering.ts 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} +
+ )} +
+

{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 && ( +
+ + {t('presentation.manageSeries')} + +
+ )}
) } @@ -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 ( +
+

Loading authors...

+
+ ) +} + +function ErrorState({ message }: { message: string }) { + return ( +
+

{message}

+
+ ) +} + +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 {