diff --git a/components/ArticlesList.tsx b/components/ArticlesList.tsx index 84232f8..5b6bbb3 100644 --- a/components/ArticlesList.tsx +++ b/components/ArticlesList.tsx @@ -1,5 +1,6 @@ import type { Article } from '@/types/nostr' import { ArticleCard } from './ArticleCard' +import { t } from '@/lib/i18n' interface ArticlesListProps { articles: Article[] @@ -13,7 +14,7 @@ interface ArticlesListProps { function LoadingState() { return (
-

Loading articles...

+

{t('common.loading.articles')}

) } diff --git a/components/AuthorPresentationEditor.tsx b/components/AuthorPresentationEditor.tsx index 31ced75..4fc5df2 100644 --- a/components/AuthorPresentationEditor.tsx +++ b/components/AuthorPresentationEditor.tsx @@ -1,4 +1,5 @@ import { useState, useCallback, useEffect, type FormEvent } from 'react' +import { useRouter } from 'next/router' import { useNostrAuth } from '@/hooks/useNostrAuth' import { useAuthorPresentation } from '@/hooks/useAuthorPresentation' import { ArticleField } from './ArticleField' @@ -8,6 +9,8 @@ import { RecoveryStep } from './CreateAccountModalSteps' import { UnlockAccountModal } from './UnlockAccountModal' import { ImageUploadField } from './ImageUploadField' import { PresentationFormHeader } from './PresentationFormHeader' +import { extractPresentationData } from '@/lib/presentationParsing' +import type { Article } from '@/types/nostr' import { t } from '@/lib/i18n' interface AuthorPresentationDraft { @@ -170,6 +173,19 @@ const PresentationFields = ({ ) +function DeleteButton({ onDelete, deleting }: { onDelete: () => void; deleting: boolean }) { + return ( + + ) +} + function PresentationForm({ draft, setDraft, @@ -177,6 +193,9 @@ function PresentationForm({ error, loading, handleSubmit, + deleting, + handleDelete, + hasExistingPresentation, }: { draft: AuthorPresentationDraft setDraft: (next: AuthorPresentationDraft) => void @@ -184,6 +203,9 @@ function PresentationForm({ error: string | null loading: boolean handleSubmit: (e: FormEvent) => Promise + deleting: boolean + handleDelete: () => void + hasExistingPresentation: boolean }) { return (
- +
+
+ +
+ {hasExistingPresentation && ( + + )} +
) } -function useAuthorPresentationState(pubkey: string | null, existingAuthorName?: string) { - const { loading, error, success, publishPresentation } = useAuthorPresentation(pubkey) - const [draft, setDraft] = useState({ - authorName: existingAuthorName ?? '', - presentation: '', - contentDescription: '', - mainnetAddress: '', +function useAuthorPresentationState(pubkey: string | null, existingAuthorName?: string, existingPresentation?: Article | null) { + const { loading, error, success, publishPresentation, deletePresentation } = useAuthorPresentation(pubkey) + const router = useRouter() + const [draft, setDraft] = useState(() => { + if (existingPresentation) { + const { presentation, contentDescription } = extractPresentationData(existingPresentation) + const authorName = existingPresentation.title.replace(/^Présentation de /, '') || existingAuthorName || '' + return { + authorName, + presentation, + contentDescription, + mainnetAddress: existingPresentation.mainnetAddress || '', + pictureUrl: existingPresentation.bannerUrl, + } + } + return { + authorName: existingAuthorName ?? '', + presentation: '', + contentDescription: '', + mainnetAddress: '', + } }) const [validationError, setValidationError] = useState(null) + const [deleting, setDeleting] = useState(false) // Update authorName when profile changes useEffect(() => { - if (existingAuthorName && existingAuthorName !== draft.authorName) { + if (existingAuthorName && existingAuthorName !== draft.authorName && !existingPresentation) { setDraft((prev) => ({ ...prev, authorName: existingAuthorName })) } - }, [existingAuthorName]) + }, [existingAuthorName, existingPresentation]) const handleSubmit = useCallback( async (e: FormEvent) => { @@ -235,7 +289,28 @@ function useAuthorPresentationState(pubkey: string | null, existingAuthorName?: [draft, publishPresentation] ) - return { loading, error, success, draft, setDraft, validationError, handleSubmit } + const handleDelete = useCallback(async () => { + if (!existingPresentation?.id) { + return + } + + if (!confirm(t('presentation.delete.confirm'))) { + return + } + + setDeleting(true) + setValidationError(null) + try { + await deletePresentation(existingPresentation.id) + await router.push('/') + } catch (e) { + setValidationError(e instanceof Error ? e.message : t('presentation.delete.error')) + } finally { + setDeleting(false) + } + }, [existingPresentation, deletePresentation, router]) + + return { loading, error, success, draft, setDraft, validationError, handleSubmit, deleting, handleDelete, existingPresentation } } function NoAccountActionButtons({ @@ -348,11 +423,42 @@ function AuthorPresentationFormView({ pubkey: string | null profile: { name?: string; pubkey: string } | null }) { - const state = useAuthorPresentationState(pubkey, profile?.name) + const { checkPresentationExists } = useAuthorPresentation(pubkey) + const [existingPresentation, setExistingPresentation] = useState
(null) + const [loadingPresentation, setLoadingPresentation] = useState(true) + + useEffect(() => { + const load = async () => { + if (!pubkey) { + setLoadingPresentation(false) + return + } + try { + const presentation = await checkPresentationExists() + setExistingPresentation(presentation) + } catch (e) { + console.error('Error loading presentation:', e) + } finally { + setLoadingPresentation(false) + } + } + void load() + }, [pubkey, checkPresentationExists]) + + const state = useAuthorPresentationState(pubkey, profile?.name, existingPresentation) if (!pubkey) { return } + + if (loadingPresentation) { + return ( +
+

{t('common.loading')}

+
+ ) + } + if (state.success) { return } @@ -365,6 +471,9 @@ function AuthorPresentationFormView({ error={state.error} loading={state.loading} handleSubmit={state.handleSubmit} + deleting={state.deleting} + handleDelete={state.handleDelete} + hasExistingPresentation={!!existingPresentation} /> ) } diff --git a/components/AuthorsList.tsx b/components/AuthorsList.tsx index 78f6335..bdeb91f 100644 --- a/components/AuthorsList.tsx +++ b/components/AuthorsList.tsx @@ -1,5 +1,6 @@ import type { Article } from '@/types/nostr' import { AuthorCard } from './AuthorCard' +import { t } from '@/lib/i18n' interface AuthorsListProps { authors: Article[] @@ -11,7 +12,7 @@ interface AuthorsListProps { function LoadingState() { return (
-

Loading authors...

+

{t('common.loading.authors')}

) } diff --git a/components/ConditionalPublishButton.tsx b/components/ConditionalPublishButton.tsx index 3f946ef..d65ea09 100644 --- a/components/ConditionalPublishButton.tsx +++ b/components/ConditionalPublishButton.tsx @@ -1,8 +1,10 @@ import Link from 'next/link' +import Image from 'next/image' import { useNostrAuth } from '@/hooks/useNostrAuth' import { useAuthorPresentation } from '@/hooks/useAuthorPresentation' import { useEffect, useState } from 'react' import { t } from '@/lib/i18n' +import type { Article } from '@/types/nostr' const buttonClassName = 'px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg text-sm font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan' @@ -30,19 +32,51 @@ function LoadingButton() { ) } +function AuthorProfileLink({ presentation, profile }: { presentation: Article; profile: { name?: string; picture?: string } | null }) { + const authorName = presentation.title.replace(/^Présentation de /, '') || profile?.name || 'Auteur' + const picture = presentation.bannerUrl || profile?.picture + + return ( + + {picture ? ( +
+ {authorName} +
+ ) : ( +
+ {authorName.charAt(0).toUpperCase()} +
+ )} + {authorName} + + ) +} + export function ConditionalPublishButton() { - const { connected, pubkey } = useNostrAuth() + const { connected, pubkey, profile } = useNostrAuth() const { checkPresentationExists } = useAuthorPresentation(pubkey ?? null) - const [hasPresentation, setHasPresentation] = useState(null) + const [presentation, setPresentation] = useState
(null) + const [loading, setLoading] = useState(false) useEffect(() => { const check = async () => { if (!connected || !pubkey) { - setHasPresentation(null) + setPresentation(null) + setLoading(false) return } - const presentation = await checkPresentationExists() - setHasPresentation(presentation !== null) + setLoading(true) + const pres = await checkPresentationExists() + setPresentation(pres) + setLoading(false) } void check() }, [connected, pubkey, checkPresentationExists]) @@ -51,13 +85,14 @@ export function ConditionalPublishButton() { return } - if (hasPresentation === null) { + if (loading) { return } - if (!hasPresentation) { + if (!presentation) { return } - return + // If presentation exists, show author profile link instead of publish button + return } diff --git a/features/account-creation-buttons-separation.md b/features/account-creation-buttons-separation.md index 5cc26b7..49ec719 100644 --- a/features/account-creation-buttons-separation.md +++ b/features/account-creation-buttons-separation.md @@ -54,3 +54,4 @@ Aucun déploiement spécial nécessaire. Les modifications sont purement fronten + diff --git a/hooks/useAuthorPresentation.ts b/hooks/useAuthorPresentation.ts index 4216a68..837b7ee 100644 --- a/hooks/useAuthorPresentation.ts +++ b/hooks/useAuthorPresentation.ts @@ -92,11 +92,38 @@ export function useAuthorPresentation(pubkey: string | null) { } } + const deletePresentation = async (articleId: string): Promise => { + if (!pubkey) { + throw new Error('Clé publique non disponible') + } + + setLoading(true) + setError(null) + + try { + const privateKey = nostrService.getPrivateKey() + if (!privateKey) { + throw new Error('Clé privée requise pour supprimer. Veuillez vous connecter avec un portefeuille Nostr qui fournit des capacités de signature.') + } + + const { deleteArticleEvent } = await import('@/lib/articleMutations') + await deleteArticleEvent(articleId, pubkey, privateKey) + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Erreur inconnue' + console.error('Error deleting presentation:', e) + setError(errorMessage) + throw e + } finally { + setLoading(false) + } + } + return { loading, error, success, publishPresentation, checkPresentationExists, + deletePresentation, } } diff --git a/lib/presentationParsing.ts b/lib/presentationParsing.ts new file mode 100644 index 0000000..702395f --- /dev/null +++ b/lib/presentationParsing.ts @@ -0,0 +1,30 @@ +import type { Article } from '@/types/nostr' + +/** + * Extract presentation data from article content + * Content format: "${presentation}\n\n---\n\nDescription du contenu :\n${contentDescription}" + */ +export function extractPresentationData(presentation: Article): { + presentation: string + contentDescription: string +} { + const content = presentation.content + const separator = '\n\n---\n\nDescription du contenu :\n' + const separatorIndex = content.indexOf(separator) + + if (separatorIndex === -1) { + // Fallback: return content as presentation if separator not found + return { + presentation: content, + contentDescription: '', + } + } + + const presentationText = content.substring(0, separatorIndex) + const contentDescription = content.substring(separatorIndex + separator.length) + + return { + presentation: presentationText, + contentDescription, + } +} diff --git a/locales/en.txt b/locales/en.txt index 29f1dca..1d5b724 100644 --- a/locales/en.txt +++ b/locales/en.txt @@ -83,6 +83,11 @@ presentation.field.mainnetAddress.placeholder=1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa presentation.field.mainnetAddress.help=Bitcoin mainnet address where you will receive sponsoring payments (0.046 BTC excluding fees per sponsoring) presentation.validation.invalidAddress=Invalid Bitcoin address (must start with 1, 3 or bc1) presentation.fallback.user=User +presentation.update.button=Update author page +presentation.delete.button=Delete author page +presentation.delete.confirm=Are you sure you want to delete your author page? This action is irreversible. +presentation.delete.deleting=Deleting... +presentation.delete.error=Error deleting author page # Filters filters.clear=Clear all @@ -102,6 +107,8 @@ footer.privacy=Privacy Policy # Common common.loading=Loading... +common.loading.articles=Loading articles... +common.loading.authors=Loading authors... common.error=Error common.back=Back common.open=Open diff --git a/locales/fr.txt b/locales/fr.txt index 200e813..ee63983 100644 --- a/locales/fr.txt +++ b/locales/fr.txt @@ -83,6 +83,11 @@ presentation.field.mainnetAddress.placeholder=1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa presentation.field.mainnetAddress.help=Adresse Bitcoin mainnet où vous recevrez les paiements de sponsoring (0.046 BTC hors frais par sponsoring) presentation.validation.invalidAddress=Adresse Bitcoin invalide (doit commencer par 1, 3 ou bc1) presentation.fallback.user=Utilisateur +presentation.update.button=Mettre à jour la page auteur +presentation.delete.button=Supprimer la page auteur +presentation.delete.confirm=Êtes-vous sûr de vouloir supprimer votre page auteur ? Cette action est irréversible. +presentation.delete.deleting=Suppression... +presentation.delete.error=Erreur lors de la suppression de la page auteur # Filters filters.clear=Effacer tout @@ -102,6 +107,8 @@ footer.privacy=Politique de confidentialité # Common common.loading=Chargement... +common.loading.articles=Chargement des articles... +common.loading.authors=Chargement des auteurs... common.error=Erreur common.back=Retour common.open=Ouvrir diff --git a/pages/presentation.tsx b/pages/presentation.tsx index c615398..7b98311 100644 --- a/pages/presentation.tsx +++ b/pages/presentation.tsx @@ -18,7 +18,7 @@ function usePresentationRedirect(connected: boolean, pubkey: string | null) { } const presentation = await checkPresentationExists() if (presentation) { - await router.push('/') + await router.push(`/author/${pubkey}`) } }, [checkPresentationExists, connected, pubkey, router]) diff --git a/public/locales/en.txt b/public/locales/en.txt index 29f1dca..1d5b724 100644 --- a/public/locales/en.txt +++ b/public/locales/en.txt @@ -83,6 +83,11 @@ presentation.field.mainnetAddress.placeholder=1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa presentation.field.mainnetAddress.help=Bitcoin mainnet address where you will receive sponsoring payments (0.046 BTC excluding fees per sponsoring) presentation.validation.invalidAddress=Invalid Bitcoin address (must start with 1, 3 or bc1) presentation.fallback.user=User +presentation.update.button=Update author page +presentation.delete.button=Delete author page +presentation.delete.confirm=Are you sure you want to delete your author page? This action is irreversible. +presentation.delete.deleting=Deleting... +presentation.delete.error=Error deleting author page # Filters filters.clear=Clear all @@ -102,6 +107,8 @@ footer.privacy=Privacy Policy # Common common.loading=Loading... +common.loading.articles=Loading articles... +common.loading.authors=Loading authors... common.error=Error common.back=Back common.open=Open diff --git a/public/locales/fr.txt b/public/locales/fr.txt index 200e813..ee63983 100644 --- a/public/locales/fr.txt +++ b/public/locales/fr.txt @@ -83,6 +83,11 @@ presentation.field.mainnetAddress.placeholder=1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa presentation.field.mainnetAddress.help=Adresse Bitcoin mainnet où vous recevrez les paiements de sponsoring (0.046 BTC hors frais par sponsoring) presentation.validation.invalidAddress=Adresse Bitcoin invalide (doit commencer par 1, 3 ou bc1) presentation.fallback.user=Utilisateur +presentation.update.button=Mettre à jour la page auteur +presentation.delete.button=Supprimer la page auteur +presentation.delete.confirm=Êtes-vous sûr de vouloir supprimer votre page auteur ? Cette action est irréversible. +presentation.delete.deleting=Suppression... +presentation.delete.error=Erreur lors de la suppression de la page auteur # Filters filters.clear=Effacer tout @@ -102,6 +107,8 @@ footer.privacy=Politique de confidentialité # Common common.loading=Chargement... +common.loading.articles=Chargement des articles... +common.loading.authors=Chargement des auteurs... common.error=Erreur common.back=Retour common.open=Ouvrir