Fix: profil image2
This commit is contained in:
parent
3009c4664f
commit
4787bd5410
42
components/AuthorCard.tsx
Normal file
42
components/AuthorCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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 (
|
||||
|
||||
60
components/AuthorsList.tsx
Normal file
60
components/AuthorsList.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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} />
|
||||
)}
|
||||
|
||||
<ArticlesList {...articlesListProps} />
|
||||
{shouldShowAuthors ? (
|
||||
<AuthorsList {...authorsListProps} />
|
||||
) : (
|
||||
<ArticlesList {...articlesListProps} />
|
||||
)}
|
||||
|
||||
<HomeIntroSection />
|
||||
</div>
|
||||
|
||||
@ -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
63
lib/authorFiltering.ts
Normal 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
|
||||
})
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user