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.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