Fix: profil image2

This commit is contained in:
Nicolas Cantu 2026-01-05 23:24:10 +01:00
parent 3009c4664f
commit 4787bd5410
12 changed files with 213 additions and 5 deletions

42
components/AuthorCard.tsx Normal file
View File

@ -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 (
<Link
href={`/author/${presentation.pubkey}`}
className="block border border-neon-cyan/20 rounded-lg p-6 bg-cyber-dark/50 hover:bg-cyber-dark hover:border-neon-cyan/40 transition-all hover:shadow-glow-cyan"
>
<div className="flex items-start gap-4">
{presentation.bannerUrl && (
<div className="relative w-20 h-20 rounded-lg overflow-hidden border border-neon-cyan/20 flex-shrink-0">
<Image
src={presentation.bannerUrl}
alt={authorName}
fill
className="object-cover"
/>
</div>
)}
<div className="flex-1 min-w-0">
<h3 className="text-xl font-bold text-neon-cyan mb-2 truncate">{authorName}</h3>
<p className="text-cyber-accent text-sm line-clamp-2 mb-3">{presentation.preview}</p>
{presentation.totalSponsoring !== undefined && presentation.totalSponsoring > 0 && (
<div className="text-xs text-neon-green">
{t('author.sponsoring.total', { amount: totalBTC.toFixed(6) })} BTC
</div>
)}
</div>
</div>
</Link>
)
}

View File

@ -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 (
<div className="border border-neon-green/50 rounded-lg p-6 bg-neon-green/10">
<h3 className="text-lg font-semibold text-neon-green mb-2">{t('presentation.success')}</h3>
<p className="text-cyber-accent">
<p className="text-cyber-accent mb-4">
{t('presentation.successMessage')}
</p>
{pubkey && (
<div className="mt-4">
<a
href={`/author/${pubkey}`}
className="inline-block px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan"
>
{t('presentation.manageSeries')}
</a>
</div>
)}
</div>
)
}
@ -344,7 +354,7 @@ function AuthorPresentationFormView({
return <NoAccountView />
}
if (state.success) {
return <SuccessNotice />
return <SuccessNotice pubkey={pubkey} />
}
return (

View File

@ -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 (
<div className="text-center py-12">
<p className="text-cyber-accent/70">Loading authors...</p>
</div>
)
}
function ErrorState({ message }: { message: string }) {
return (
<div className="bg-red-900/20 border border-red-500/50 rounded-lg p-4 mb-4">
<p className="text-red-400">{message}</p>
</div>
)
}
function EmptyState({ hasAny }: { hasAny: boolean }) {
return (
<div className="text-center py-12">
<p className="text-cyber-accent/70">
{hasAny ? 'No authors match your search or filters.' : 'No authors found. Check back later!'}
</p>
</div>
)
}
export function AuthorsList({ authors, allAuthors, loading, error }: AuthorsListProps) {
if (loading) {
return <LoadingState />
}
if (error) {
return <ErrorState message={error} />
}
if (authors.length === 0) {
return <EmptyState hasAny={allAuthors.length > 0} />
}
return (
<>
<div className="mb-4 text-sm text-cyber-accent/70">
Showing {authors.length} of {allAuthors.length} author{allAuthors.length !== 1 ? 's' : ''}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{authors.map((author) => (
<AuthorCard key={author.pubkey} presentation={author} />
))}
</div>
</>
)
}

View File

@ -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<SetStateAction<ArticleFilters>>
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 (
<div className="max-w-4xl mx-auto px-4 py-8">
@ -102,11 +109,15 @@ function HomeContent({
setSelectedCategory={setSelectedCategory}
/>
{shouldShowFilters && (
{shouldShowFilters && !shouldShowAuthors && (
<ArticleFiltersComponent filters={filters} onFiltersChange={setFilters} articles={allArticles} />
)}
{shouldShowAuthors ? (
<AuthorsList {...authorsListProps} />
) : (
<ArticlesList {...articlesListProps} />
)}
<HomeIntroSection />
</div>

View File

@ -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 } : {}),
}
}

63
lib/authorFiltering.ts Normal file
View File

@ -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<string, Article>,
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
})
}

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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 {