fix 'Chargement..' for profil

This commit is contained in:
Nicolas Cantu 2026-01-06 01:11:20 +01:00
parent 16560e0b52
commit a4a39fd0ba
14 changed files with 84 additions and 42 deletions

View File

@ -16,24 +16,15 @@ function CreateAuthorPageLink() {
) )
} }
function PublishLink() {
return (
<Link href="/publish" className={buttonClassName}>
{t('nav.publish')}
</Link>
)
}
function LoadingButton() {
return (
<div className="px-4 py-2 bg-neon-cyan/20 text-neon-cyan rounded-lg text-sm font-medium">
{t('nav.loading')}
</div>
)
}
function AuthorProfileLink({ presentation, profile }: { presentation: Article; profile: { name?: string; picture?: string } | null }) { function AuthorProfileLink({ presentation, profile }: { presentation: Article; profile: { name?: string; picture?: string } | null }) {
const authorName = presentation.title.replace(/^Présentation de /, '') || profile?.name || 'Auteur' // Extract author name from presentation title or profile
// Title format: "Présentation de <name>" or just use profile name
let authorName = presentation.title.replace(/^Présentation de /, '').trim()
if (!authorName || authorName === 'Présentation') {
authorName = profile?.name || 'Auteur'
}
// Extract picture from presentation (bannerUrl or from JSON metadata) or profile
const picture = presentation.bannerUrl || profile?.picture const picture = presentation.bannerUrl || profile?.picture
return ( return (
@ -48,6 +39,7 @@ function AuthorProfileLink({ presentation, profile }: { presentation: Article; p
alt={authorName} alt={authorName}
fill fill
className="object-cover" className="object-cover"
sizes="32px"
/> />
</div> </div>
) : ( ) : (
@ -64,19 +56,16 @@ export function ConditionalPublishButton() {
const { connected, pubkey, profile } = useNostrAuth() const { connected, pubkey, profile } = useNostrAuth()
const { checkPresentationExists } = useAuthorPresentation(pubkey ?? null) const { checkPresentationExists } = useAuthorPresentation(pubkey ?? null)
const [presentation, setPresentation] = useState<Article | null>(null) const [presentation, setPresentation] = useState<Article | null>(null)
const [loading, setLoading] = useState(false)
useEffect(() => { useEffect(() => {
const check = async () => { const check = async () => {
if (!connected || !pubkey) { if (!connected || !pubkey) {
setPresentation(null) setPresentation(null)
setLoading(false)
return return
} }
setLoading(true) // Check for presentation asynchronously, but don't show loading state
const pres = await checkPresentationExists() const pres = await checkPresentationExists()
setPresentation(pres) setPresentation(pres)
setLoading(false)
} }
void check() void check()
}, [connected, pubkey, checkPresentationExists]) }, [connected, pubkey, checkPresentationExists])
@ -85,14 +74,11 @@ export function ConditionalPublishButton() {
return <CreateAuthorPageLink /> return <CreateAuthorPageLink />
} }
if (loading) { // If presentation exists, show author profile link with image/fallback
return <LoadingButton /> if (presentation) {
}
if (!presentation) {
return <CreateAuthorPageLink />
}
// If presentation exists, show author profile link instead of publish button
return <AuthorProfileLink presentation={presentation} profile={profile} /> return <AuthorProfileLink presentation={presentation} profile={profile} />
} }
// Otherwise, show create author page button immediately (no loading state)
return <CreateAuthorPageLink />
}

View File

@ -1,5 +1,5 @@
import React from 'react'
import { renderMarkdown } from '@/lib/markdownRenderer' import { renderMarkdown } from '@/lib/markdownRenderer'
import { t } from '@/lib/i18n'
interface DocsContentProps { interface DocsContentProps {
content: string content: string
@ -10,7 +10,7 @@ export function DocsContent({ content, loading }: DocsContentProps) {
if (loading) { if (loading) {
return ( return (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-cyber-accent">Chargement de la documentation...</p> <p className="text-cyber-accent">{t('docs.loading')}</p>
</div> </div>
) )
} }

View File

@ -219,13 +219,13 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
<div <div
className="text-neon-cyan cursor-pointer hover:text-neon-green transition-colors" className="text-neon-cyan cursor-pointer hover:text-neon-green transition-colors"
onClick={() => setEditingId(api.id)} onClick={() => setEditingId(api.id)}
title="Click to edit URL" title={t('settings.nip95.list.editUrl')}
> >
{api.url} {api.url}
</div> </div>
)} )}
<div className="text-sm text-cyber-accent mt-1"> <div className="text-sm text-cyber-accent mt-1">
Priority: {api.priority} | ID: {api.id} {t('settings.nip95.list.priorityLabel', { priority: api.priority, id: api.id })}
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -273,11 +273,10 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps)
<div className="text-sm text-cyber-accent space-y-2"> <div className="text-sm text-cyber-accent space-y-2">
<p> <p>
<strong>Note:</strong> Endpoints are tried in priority order (lower number = higher priority). <strong>{t('settings.nip95.note.title')}</strong> {t('settings.nip95.note.priority')}
Only enabled endpoints will be used for uploads.
</p> </p>
<p> <p>
If an endpoint fails, the next enabled endpoint will be tried automatically. {t('settings.nip95.note.fallback')}
</p> </p>
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@ import type { AuthorPresentationDraft } from './articlePublisher'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
import { buildTags, extractTagsFromEvent, buildTagFilter } from './nostrTagSystem' import { buildTags, extractTagsFromEvent, buildTagFilter } from './nostrTagSystem'
import { getPrimaryRelaySync } from './config' import { getPrimaryRelaySync } from './config'
import { PLATFORM_SERVICE } from './platformConfig' import { PLATFORM_SERVICE, MIN_EVENT_DATE } from './platformConfig'
import { generateAuthorHashId } from './hashIdGenerator' import { generateAuthorHashId } from './hashIdGenerator'
import { generateObjectUrl } from './urlGenerator' import { generateObjectUrl } from './urlGenerator'
import { getLatestVersion } from './versionManager' import { getLatestVersion } from './versionManager'
@ -104,6 +104,7 @@ export function parsePresentationEvent(event: Event): import('@/types/nostr').Au
// Try to extract profile JSON from tag first (new format) // Try to extract profile JSON from tag first (new format)
let profileData: { let profileData: {
authorName?: string
presentation?: string presentation?: string
contentDescription?: string contentDescription?: string
mainnetAddress?: string mainnetAddress?: string
@ -191,6 +192,7 @@ export async function fetchAuthorPresentationFromPool(
authorPubkey: pubkey, authorPubkey: pubkey,
service: PLATFORM_SERVICE, service: PLATFORM_SERVICE,
}), }),
since: MIN_EVENT_DATE,
limit: 100, // Get all versions to find the latest limit: 100, // Get all versions to find the latest
}, },
] ]

View File

@ -6,7 +6,7 @@ import type { Article } from '@/types/nostr'
import { parseArticleFromEvent } from './nostrEventParsing' import { parseArticleFromEvent } from './nostrEventParsing'
import { buildTagFilter } from './nostrTagSystem' import { buildTagFilter } from './nostrTagSystem'
import { getPrimaryRelaySync } from './config' import { getPrimaryRelaySync } from './config'
import { PLATFORM_SERVICE } from './platformConfig' import { PLATFORM_SERVICE, MIN_EVENT_DATE } from './platformConfig'
function createSeriesSubscription(pool: SimplePool, seriesId: string, limit: number) { function createSeriesSubscription(pool: SimplePool, seriesId: string, limit: number) {
const filters = [ const filters = [
@ -16,6 +16,7 @@ function createSeriesSubscription(pool: SimplePool, seriesId: string, limit: num
seriesId, seriesId,
service: PLATFORM_SERVICE, service: PLATFORM_SERVICE,
}), }),
since: MIN_EVENT_DATE,
limit, limit,
}, },
] ]

View File

@ -13,7 +13,7 @@ import { checkZapReceipt as checkZapReceiptHelper } from './nostrZapVerification
import { subscribeWithTimeout } from './nostrSubscription' import { subscribeWithTimeout } from './nostrSubscription'
import { getPrimaryRelay, getPrimaryRelaySync } from './config' import { getPrimaryRelay, getPrimaryRelaySync } from './config'
import { buildTagFilter } from './nostrTagSystem' import { buildTagFilter } from './nostrTagSystem'
import { PLATFORM_SERVICE } from './platformConfig' import { PLATFORM_SERVICE, MIN_EVENT_DATE } from './platformConfig'
class NostrService { class NostrService {
private pool: SimplePool | null = null private pool: SimplePool | null = null
@ -89,12 +89,14 @@ class NostrService {
// Subscribe to both 'publication' and 'author' type events // Subscribe to both 'publication' and 'author' type events
// Authors are identified by tag type='author' in the tag system // Authors are identified by tag type='author' in the tag system
// Filter by service='zapwall.fr' to only get notes from this platform // Filter by service='zapwall.fr' to only get notes from this platform
// Limit to events published on or after January 6, 2026
const filters = [ const filters = [
{ {
...buildTagFilter({ ...buildTagFilter({
type: 'publication', type: 'publication',
service: PLATFORM_SERVICE, service: PLATFORM_SERVICE,
}), }),
since: MIN_EVENT_DATE,
limit, limit,
}, },
{ {
@ -102,6 +104,7 @@ class NostrService {
type: 'author', type: 'author',
service: PLATFORM_SERVICE, service: PLATFORM_SERVICE,
}), }),
since: MIN_EVENT_DATE,
limit, limit,
}, },
] ]

View File

@ -1,3 +1,7 @@
export const PLATFORM_NPUB = 'npub18s03s39fa80ce2n3cmm0zme3jqehc82h6ld9sxq03uejqm3d05gsae0fuu' export const PLATFORM_NPUB = 'npub18s03s39fa80ce2n3cmm0zme3jqehc82h6ld9sxq03uejqm3d05gsae0fuu'
export const PLATFORM_BITCOIN_ADDRESS = 'bc1qerauk5yhqytl6z93ckvwkylup8s0256uenzg9y' export const PLATFORM_BITCOIN_ADDRESS = 'bc1qerauk5yhqytl6z93ckvwkylup8s0256uenzg9y'
export const PLATFORM_SERVICE = 'zapwall.fr' export const PLATFORM_SERVICE = 'zapwall.fr'
// Minimum date for Nostr events: January 6, 2026 00:00:00 UTC
// Timestamp Unix: 1736115200
export const MIN_EVENT_DATE = 1736115200

View File

@ -4,7 +4,7 @@ import type { Review } from '@/types/nostr'
import { parseReviewFromEvent } from './nostrEventParsing' import { parseReviewFromEvent } from './nostrEventParsing'
import { buildTagFilter } from './nostrTagSystem' import { buildTagFilter } from './nostrTagSystem'
import { getPrimaryRelaySync } from './config' import { getPrimaryRelaySync } from './config'
import { PLATFORM_SERVICE } from './platformConfig' import { PLATFORM_SERVICE, MIN_EVENT_DATE } from './platformConfig'
function buildReviewFilters(articleId: string) { function buildReviewFilters(articleId: string) {
const tagFilter = buildTagFilter({ const tagFilter = buildTagFilter({
@ -17,8 +17,10 @@ function buildReviewFilters(articleId: string) {
kinds: number[] kinds: number[]
'#quote'?: string[] '#quote'?: string[]
'#article'?: string[] '#article'?: string[]
since?: number
} = { } = {
kinds: Array.isArray(tagFilter.kinds) ? tagFilter.kinds as number[] : [1], kinds: Array.isArray(tagFilter.kinds) ? tagFilter.kinds as number[] : [1],
since: MIN_EVENT_DATE,
} }
if (tagFilter['#quote']) { if (tagFilter['#quote']) {
filterObj['#quote'] = tagFilter['#quote'] as string[] filterObj['#quote'] = tagFilter['#quote'] as string[]

View File

@ -4,7 +4,7 @@ import type { Series } from '@/types/nostr'
import { parseSeriesFromEvent } from './nostrEventParsing' import { parseSeriesFromEvent } from './nostrEventParsing'
import { buildTagFilter } from './nostrTagSystem' import { buildTagFilter } from './nostrTagSystem'
import { getPrimaryRelaySync } from './config' import { getPrimaryRelaySync } from './config'
import { PLATFORM_SERVICE } from './platformConfig' import { PLATFORM_SERVICE, MIN_EVENT_DATE } from './platformConfig'
function buildSeriesFilters(authorPubkey: string) { function buildSeriesFilters(authorPubkey: string) {
const tagFilter = buildTagFilter({ const tagFilter = buildTagFilter({
@ -18,6 +18,7 @@ function buildSeriesFilters(authorPubkey: string) {
kinds: tagFilter.kinds as number[], kinds: tagFilter.kinds as number[],
...(tagFilter.authors ? { authors: tagFilter.authors as string[] } : {}), ...(tagFilter.authors ? { authors: tagFilter.authors as string[] } : {}),
...(tagFilter['#series'] ? { '#series': tagFilter['#series'] as string[] } : {}), ...(tagFilter['#series'] ? { '#series': tagFilter['#series'] as string[] } : {}),
since: MIN_EVENT_DATE,
}, },
] ]
} }
@ -66,6 +67,7 @@ function buildSeriesByIdFilters(seriesId: string) {
type: 'series', type: 'series',
service: PLATFORM_SERVICE, service: PLATFORM_SERVICE,
}), }),
since: MIN_EVENT_DATE,
}, },
] ]
} }

View File

@ -2,7 +2,7 @@ import { nostrService } from './nostr'
import type { Article } from '@/types/nostr' import type { Article } from '@/types/nostr'
import { getPrimaryRelaySync } from './config' import { getPrimaryRelaySync } from './config'
import { buildTagFilter, extractTagsFromEvent } from './nostrTagSystem' import { buildTagFilter, extractTagsFromEvent } from './nostrTagSystem'
import { PLATFORM_SERVICE } from './platformConfig' import { PLATFORM_SERVICE, MIN_EVENT_DATE } from './platformConfig'
function subscribeToPresentation(pool: import('nostr-tools').SimplePool, pubkey: string): Promise<number> { function subscribeToPresentation(pool: import('nostr-tools').SimplePool, pubkey: string): Promise<number> {
const filters = [ const filters = [
@ -12,6 +12,7 @@ function subscribeToPresentation(pool: import('nostr-tools').SimplePool, pubkey:
authorPubkey: pubkey, authorPubkey: pubkey,
service: PLATFORM_SERVICE, service: PLATFORM_SERVICE,
}), }),
since: MIN_EVENT_DATE,
limit: 1, limit: 1,
}, },
] ]

View File

@ -192,3 +192,8 @@ settings.nip95.list.cancel=Cancel
settings.nip95.list.remove=Remove settings.nip95.list.remove=Remove
settings.nip95.remove.confirm=Are you sure you want to remove this endpoint? settings.nip95.remove.confirm=Are you sure you want to remove this endpoint?
settings.nip95.empty=No endpoints configured settings.nip95.empty=No endpoints configured
settings.nip95.list.priorityLabel=Priority: {{priority}} | ID: {{id}}
settings.nip95.list.editUrl=Click to edit URL
settings.nip95.note.title=Note:
settings.nip95.note.priority=Endpoints are tried in priority order (lower number = higher priority). Only enabled endpoints will be used for uploads.
settings.nip95.note.fallback=If an endpoint fails, the next enabled endpoint will be tried automatically.

View File

@ -192,3 +192,8 @@ settings.nip95.list.cancel=Annuler
settings.nip95.list.remove=Supprimer settings.nip95.list.remove=Supprimer
settings.nip95.remove.confirm=Êtes-vous sûr de vouloir supprimer cet endpoint ? settings.nip95.remove.confirm=Êtes-vous sûr de vouloir supprimer cet endpoint ?
settings.nip95.empty=Aucun endpoint configuré settings.nip95.empty=Aucun endpoint configuré
settings.nip95.list.priorityLabel=Priorité: {{priority}} | ID: {{id}}
settings.nip95.list.editUrl=Cliquer pour modifier l'URL
settings.nip95.note.title=Note :
settings.nip95.note.priority=Les endpoints sont essayés dans l'ordre de priorité (nombre plus bas = priorité plus haute). Seuls les endpoints activés seront utilisés pour les uploads.
settings.nip95.note.fallback=Si un endpoint échoue, le prochain endpoint activé sera essayé automatiquement.

View File

@ -18,6 +18,17 @@ nav.publish=Publish profile
nav.createAuthorPage=Create author page nav.createAuthorPage=Create author page
nav.loading=Loading... nav.loading=Loading...
# Documentation
docs.title=Documentation
docs.userGuide=User Guide
docs.faq=FAQ
docs.publishing=Publishing Guide
docs.payment=Payment Guide
docs.error=Error
docs.error.loadFailed=Unable to load documentation.
docs.meta.description=Complete documentation for zapwall.fr
docs.loading=Loading documentation...
# Categories # Categories
category.science-fiction=Science Fiction category.science-fiction=Science Fiction
category.scientific-research=Scientific Research category.scientific-research=Scientific Research
@ -182,3 +193,8 @@ settings.nip95.list.cancel=Cancel
settings.nip95.list.remove=Remove settings.nip95.list.remove=Remove
settings.nip95.remove.confirm=Are you sure you want to remove this endpoint? settings.nip95.remove.confirm=Are you sure you want to remove this endpoint?
settings.nip95.empty=No endpoints configured settings.nip95.empty=No endpoints configured
settings.nip95.list.priorityLabel=Priority: {{priority}} | ID: {{id}}
settings.nip95.list.editUrl=Click to edit URL
settings.nip95.note.title=Note:
settings.nip95.note.priority=Endpoints are tried in priority order (lower number = higher priority). Only enabled endpoints will be used for uploads.
settings.nip95.note.fallback=If an endpoint fails, the next enabled endpoint will be tried automatically.

View File

@ -18,6 +18,17 @@ nav.publish=Publier le profil
nav.createAuthorPage=Créer page auteur nav.createAuthorPage=Créer page auteur
nav.loading=Chargement... nav.loading=Chargement...
# Documentation
docs.title=Documentation
docs.userGuide=Guide d'utilisation
docs.faq=FAQ
docs.publishing=Guide de publication
docs.payment=Guide de paiement
docs.error=Erreur
docs.error.loadFailed=Impossible de charger la documentation.
docs.meta.description=Documentation complète pour zapwall.fr
docs.loading=Chargement de la documentation...
# Categories # Categories
category.science-fiction=Science-fiction category.science-fiction=Science-fiction
category.scientific-research=Recherche scientifique category.scientific-research=Recherche scientifique
@ -182,3 +193,8 @@ settings.nip95.list.cancel=Annuler
settings.nip95.list.remove=Supprimer settings.nip95.list.remove=Supprimer
settings.nip95.remove.confirm=Êtes-vous sûr de vouloir supprimer cet endpoint ? settings.nip95.remove.confirm=Êtes-vous sûr de vouloir supprimer cet endpoint ?
settings.nip95.empty=Aucun endpoint configuré settings.nip95.empty=Aucun endpoint configuré
settings.nip95.list.priorityLabel=Priorité: {{priority}} | ID: {{id}}
settings.nip95.list.editUrl=Cliquer pour modifier l'URL
settings.nip95.note.title=Note :
settings.nip95.note.priority=Les endpoints sont essayés dans l'ordre de priorité (nombre plus bas = priorité plus haute). Seuls les endpoints activés seront utilisés pour les uploads.
settings.nip95.note.fallback=Si un endpoint échoue, le prochain endpoint activé sera essayé automatiquement.