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}$/
|
const ADDRESS_PATTERN = /^(1|3|bc1)[a-zA-Z0-9]{25,62}$/
|
||||||
|
|
||||||
function SuccessNotice() {
|
function SuccessNotice({ pubkey }: { pubkey: string | null }) {
|
||||||
return (
|
return (
|
||||||
<div className="border border-neon-green/50 rounded-lg p-6 bg-neon-green/10">
|
<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>
|
<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')}
|
{t('presentation.successMessage')}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -344,7 +354,7 @@ function AuthorPresentationFormView({
|
|||||||
return <NoAccountView />
|
return <NoAccountView />
|
||||||
}
|
}
|
||||||
if (state.success) {
|
if (state.success) {
|
||||||
return <SuccessNotice />
|
return <SuccessNotice pubkey={pubkey} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 { CategoryTabs } from '@/components/CategoryTabs'
|
||||||
import { SearchBar } from '@/components/SearchBar'
|
import { SearchBar } from '@/components/SearchBar'
|
||||||
import { ArticlesList } from '@/components/ArticlesList'
|
import { ArticlesList } from '@/components/ArticlesList'
|
||||||
|
import { AuthorsList } from '@/components/AuthorsList'
|
||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { Footer } from '@/components/Footer'
|
import { Footer } from '@/components/Footer'
|
||||||
import { FundingGauge } from '@/components/FundingGauge'
|
import { FundingGauge } from '@/components/FundingGauge'
|
||||||
@ -18,6 +19,8 @@ interface HomeViewProps {
|
|||||||
setFilters: Dispatch<SetStateAction<ArticleFilters>>
|
setFilters: Dispatch<SetStateAction<ArticleFilters>>
|
||||||
articles: Article[]
|
articles: Article[]
|
||||||
allArticles: Article[]
|
allArticles: Article[]
|
||||||
|
authors: Article[]
|
||||||
|
allAuthors: Article[]
|
||||||
loading: boolean
|
loading: boolean
|
||||||
error: string | null
|
error: string | null
|
||||||
onUnlock: (article: Article) => void
|
onUnlock: (article: Article) => void
|
||||||
@ -85,13 +88,17 @@ function HomeContent({
|
|||||||
setFilters,
|
setFilters,
|
||||||
articles,
|
articles,
|
||||||
allArticles,
|
allArticles,
|
||||||
|
authors,
|
||||||
|
allAuthors,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
onUnlock,
|
onUnlock,
|
||||||
unlockedArticles,
|
unlockedArticles,
|
||||||
}: HomeViewProps) {
|
}: HomeViewProps) {
|
||||||
const shouldShowFilters = !loading && allArticles.length > 0
|
const shouldShowFilters = !loading && allArticles.length > 0
|
||||||
|
const shouldShowAuthors = selectedCategory !== null && selectedCategory !== 'all'
|
||||||
const articlesListProps = { articles, allArticles, loading, error, onUnlock, unlockedArticles }
|
const articlesListProps = { articles, allArticles, loading, error, onUnlock, unlockedArticles }
|
||||||
|
const authorsListProps = { authors, allAuthors, loading, error }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||||
@ -102,11 +109,15 @@ function HomeContent({
|
|||||||
setSelectedCategory={setSelectedCategory}
|
setSelectedCategory={setSelectedCategory}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{shouldShowFilters && (
|
{shouldShowFilters && !shouldShowAuthors && (
|
||||||
<ArticleFiltersComponent filters={filters} onFiltersChange={setFilters} articles={allArticles} />
|
<ArticleFiltersComponent filters={filters} onFiltersChange={setFilters} articles={allArticles} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ArticlesList {...articlesListProps} />
|
{shouldShowAuthors ? (
|
||||||
|
<AuthorsList {...authorsListProps} />
|
||||||
|
) : (
|
||||||
|
<ArticlesList {...articlesListProps} />
|
||||||
|
)}
|
||||||
|
|
||||||
<HomeIntroSection />
|
<HomeIntroSection />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -31,6 +31,9 @@ export function parsePresentationEvent(event: Event): import('@/types/nostr').Au
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map tag category to article category
|
||||||
|
const articleCategory = tags.category === 'sciencefiction' ? 'science-fiction' : tags.category === 'research' ? 'scientific-research' : undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: tags.id ?? event.id,
|
id: tags.id ?? event.id,
|
||||||
pubkey: event.pubkey,
|
pubkey: event.pubkey,
|
||||||
@ -44,6 +47,7 @@ export function parsePresentationEvent(event: Event): import('@/types/nostr').Au
|
|||||||
isPresentation: true,
|
isPresentation: true,
|
||||||
mainnetAddress: tags.mainnetAddress ?? '',
|
mainnetAddress: tags.mainnetAddress ?? '',
|
||||||
totalSponsoring: tags.totalSponsoring ?? 0,
|
totalSponsoring: tags.totalSponsoring ?? 0,
|
||||||
|
originalCategory: articleCategory, // Store original category for filtering
|
||||||
...(tags.pictureUrl !== undefined && tags.pictureUrl !== null && typeof tags.pictureUrl === 'string' ? { bannerUrl: tags.pictureUrl } : {}),
|
...(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.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.success=Presentation article created!
|
||||||
presentation.successMessage=Your presentation article has been created successfully. You can now publish articles.
|
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.profileNote=This profile data is specific to zapwall.fr and may differ from your Nostr profile.
|
||||||
presentation.field.picture=Profile picture
|
presentation.field.picture=Profile picture
|
||||||
presentation.field.picture.help=Profile image for your author page (max 5MB, formats: PNG, JPG, WebP)
|
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.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.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.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.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=Photo de profil
|
||||||
presentation.field.picture.help=Image de profil pour votre page auteur (max 5Mo, formats: PNG, JPG, WebP)
|
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 { useArticles } from '@/hooks/useArticles'
|
||||||
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
import { useNostrAuth } from '@/hooks/useNostrAuth'
|
||||||
import { applyFiltersAndSort } from '@/lib/articleFiltering'
|
import { applyFiltersAndSort } from '@/lib/articleFiltering'
|
||||||
|
import { getAuthorsByCategory, sortAuthors } from '@/lib/authorFiltering'
|
||||||
import type { Article } from '@/types/nostr'
|
import type { Article } from '@/types/nostr'
|
||||||
import type { ArticleFilters } from '@/components/ArticleFilters'
|
import type { ArticleFilters } from '@/components/ArticleFilters'
|
||||||
import { HomeView } from '@/components/HomeView'
|
import { HomeView } from '@/components/HomeView'
|
||||||
@ -104,6 +105,16 @@ function useHomeController() {
|
|||||||
const articles = useFilteredArticles(allArticlesRaw, searchQuery, filters, presentationArticles)
|
const articles = useFilteredArticles(allArticlesRaw, searchQuery, filters, presentationArticles)
|
||||||
const handleUnlock = useUnlockHandler(loadArticleContent, setUnlockedArticles)
|
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 {
|
return {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
@ -113,6 +124,8 @@ function useHomeController() {
|
|||||||
setFilters,
|
setFilters,
|
||||||
articles,
|
articles,
|
||||||
allArticles,
|
allArticles,
|
||||||
|
authors,
|
||||||
|
allAuthors,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
unlockedArticles,
|
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.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.success=Presentation article created!
|
||||||
presentation.successMessage=Your presentation article has been created successfully. You can now publish articles.
|
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.profileNote=This profile data is specific to zapwall.fr and may differ from your Nostr profile.
|
||||||
presentation.field.picture=Profile picture
|
presentation.field.picture=Profile picture
|
||||||
presentation.field.picture.help=Profile image for your author page (max 5MB, formats: PNG, JPG, WebP)
|
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.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.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.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.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=Photo de profil
|
||||||
presentation.field.picture.help=Image de profil pour votre page auteur (max 5Mo, formats: PNG, JPG, WebP)
|
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
|
isPresentation: true
|
||||||
mainnetAddress: string
|
mainnetAddress: string
|
||||||
totalSponsoring: number
|
totalSponsoring: number
|
||||||
|
originalCategory?: 'science-fiction' | 'scientific-research' // Original category from tags for filtering
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Series {
|
export interface Series {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user