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'
import { CreateAccountModal } from './CreateAccountModal'
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'
import { userConfirm } from '@/lib/userConfirm'
interface AuthorPresentationDraft {
authorName: string
presentation: string
contentDescription: string
mainnetAddress: string
pictureUrl?: string
}
const ADDRESS_PATTERN = /^(1|3|bc1)[a-zA-Z0-9]{25,62}$/
function SuccessNotice({ pubkey }: { pubkey: string | null }): React.ReactElement {
return (
{t('presentation.success')}
{t('presentation.successMessage')}
{pubkey && (
)}
)
}
function ValidationError({ message }: { message: string | null }): React.ReactElement | null {
if (!message) {
return null
}
return (
)
}
function PresentationField({
draft,
onChange,
}: {
draft: AuthorPresentationDraft
onChange: (next: AuthorPresentationDraft) => void
}): React.ReactElement {
return (
onChange({ ...draft, presentation: value as string })}
required
type="textarea"
rows={6}
placeholder={t('presentation.field.presentation.placeholder')}
helpText={t('presentation.field.presentation.help')}
/>
)
}
function ContentDescriptionField({
draft,
onChange,
}: {
draft: AuthorPresentationDraft
onChange: (next: AuthorPresentationDraft) => void
}): React.ReactElement {
return (
onChange({ ...draft, contentDescription: value as string })}
required
type="textarea"
rows={6}
placeholder={t('presentation.field.contentDescription.placeholder')}
helpText={t('presentation.field.contentDescription.help')}
/>
)
}
function MainnetAddressField({
draft,
onChange,
}: {
draft: AuthorPresentationDraft
onChange: (next: AuthorPresentationDraft) => void
}): React.ReactElement {
return (
onChange({ ...draft, mainnetAddress: value as string })}
required
type="text"
placeholder={t('presentation.field.mainnetAddress.placeholder')}
helpText={t('presentation.field.mainnetAddress.help')}
/>
)
}
function AuthorNameField({
draft,
onChange,
}: {
draft: AuthorPresentationDraft
onChange: (next: AuthorPresentationDraft) => void
}): React.ReactElement {
return (
onChange({ ...draft, authorName: value as string })}
required
type="text"
placeholder={t('presentation.field.authorName.placeholder')}
helpText={t('presentation.field.authorName.help')}
/>
)
}
function PictureField({
draft,
onChange,
}: {
draft: AuthorPresentationDraft
onChange: (next: AuthorPresentationDraft) => void
}): React.ReactElement {
return (
onChange({ ...draft, pictureUrl: url })}
/>
)
}
const PresentationFields = ({
draft,
onChange,
}: {
draft: AuthorPresentationDraft
onChange: (next: AuthorPresentationDraft) => void
}): React.ReactElement => (
)
function DeleteButton({ onDelete, deleting }: { onDelete: () => void; deleting: boolean }): React.ReactElement {
return (
)
}
function PresentationForm({
draft,
setDraft,
validationError,
error,
loading,
handleSubmit,
deleting,
handleDelete,
hasExistingPresentation,
}: {
draft: AuthorPresentationDraft
setDraft: (next: AuthorPresentationDraft) => void
validationError: string | null
error: string | null
loading: boolean
handleSubmit: (e: FormEvent) => Promise
deleting: boolean
handleDelete: () => void
hasExistingPresentation: boolean
}): React.ReactElement {
return (
)
}
function useAuthorPresentationState(pubkey: string | null, existingAuthorName?: string, existingPresentation?: Article | null): {
draft: AuthorPresentationDraft
setDraft: (next: AuthorPresentationDraft) => void
validationError: string | null
error: string | null
loading: boolean
handleSubmit: (e: FormEvent) => Promise
deleting: boolean
handleDelete: () => Promise
success: boolean
} {
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 ?? '',
...(existingPresentation.bannerUrl ? { 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 && !existingPresentation) {
setDraft((prev) => ({ ...prev, authorName: existingAuthorName }))
}
}, [existingAuthorName, existingPresentation, draft.authorName])
const handleSubmit = useCallback(
async (e: FormEvent) => {
e.preventDefault()
const address = draft.mainnetAddress.trim()
if (!ADDRESS_PATTERN.test(address)) {
setValidationError(t('presentation.validation.invalidAddress'))
return
}
if (!draft.authorName.trim()) {
setValidationError(t('presentation.validation.authorNameRequired'))
return
}
setValidationError(null)
await publishPresentation(draft)
},
[draft, publishPresentation]
)
const handleDelete = useCallback(async () => {
if (!existingPresentation?.id) {
return
}
const confirmed = await userConfirm(t('presentation.delete.confirm'))
if (!confirmed) {
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 }
}
function NoAccountActionButtons({
onGenerate,
onImport,
}: {
onGenerate: () => void
onImport: () => void
}): React.ReactElement {
return (
)
}
function NoAccountView(): React.ReactElement {
const [showImportModal, setShowImportModal] = useState(false)
const [showRecoveryStep, setShowRecoveryStep] = useState(false)
const [showUnlockModal, setShowUnlockModal] = useState(false)
const [recoveryPhrase, setRecoveryPhrase] = useState([])
const [npub, setNpub] = useState('')
const [generating, setGenerating] = useState(false)
const [error, setError] = useState(null)
const handleGenerate = async (): Promise => {
setGenerating(true)
setError(null)
try {
const { nostrAuthService } = await import('@/lib/nostrAuth')
const result = await nostrAuthService.createAccount()
setRecoveryPhrase(result.recoveryPhrase)
setNpub(result.npub)
setShowRecoveryStep(true)
} catch (e) {
setError(e instanceof Error ? e.message : t('account.create.error.failed'))
} finally {
setGenerating(false)
}
}
const handleRecoveryContinue = (): void => {
setShowRecoveryStep(false)
setShowUnlockModal(true)
}
const handleUnlockSuccess = (): void => {
setShowUnlockModal(false)
setRecoveryPhrase([])
setNpub('')
}
return (
Créez un compte ou importez votre clé secrète pour commencer
{error &&
{error}
}
{ void handleGenerate() }}
onImport={() => setShowImportModal(true)}
/>
{generating && (
Génération du compte...
)}
{showImportModal && (
{
setShowImportModal(false)
setShowUnlockModal(true)
}}
onClose={() => setShowImportModal(false)}
initialStep="import"
/>
)}
{showRecoveryStep && (
)}
{showUnlockModal && (
setShowUnlockModal(false)}
/>
)}
)
}
function AuthorPresentationFormView({
pubkey,
profile,
}: {
pubkey: string | null
profile: { name?: string; pubkey: string } | null
}): React.ReactElement {
const { checkPresentationExists } = useAuthorPresentation(pubkey)
const [existingPresentation, setExistingPresentation] = useState(null)
const [loadingPresentation, setLoadingPresentation] = useState(true)
useEffect(() => {
const load = async (): Promise => {
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 (
)
}
if (state.success) {
return
}
return (
{ void state.handleDelete() }}
hasExistingPresentation={existingPresentation !== null && existingPresentation !== undefined}
/>
)
}
function useAutoLoadPubkey(accountExists: boolean | null, pubkey: string | null, connect: () => Promise): void {
useEffect(() => {
if (accountExists === true && !pubkey) {
void connect()
}
}, [accountExists, pubkey, connect])
}
export function AuthorPresentationEditor(): React.ReactElement {
const { pubkey, profile, accountExists, connect } = useNostrAuth()
useAutoLoadPubkey(accountExists, pubkey ?? null, connect)
return
}