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 && (
{t('presentation.manageSeries')}
)}
) } function ValidationError({ message }: { message: string | null }): React.ReactElement | null { if (!message) { return null } return (

{message}

) } 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 ( ) } type PresentationFormProps = { draft: AuthorPresentationDraft setDraft: (next: AuthorPresentationDraft) => void validationError: string | null error: string | null loading: boolean handleSubmit: (e: FormEvent) => Promise deleting: boolean handleDelete: () => void hasExistingPresentation: boolean } function PresentationForm(props: PresentationFormProps): React.ReactElement { return (
) => { void props.handleSubmit(e) }} className="border border-neon-cyan/20 rounded-lg p-6 bg-cyber-dark space-y-4" >
{props.hasExistingPresentation && ( { void props.handleDelete() }} deleting={props.deleting} /> )}
) } function getSubmitLabel(params: { loading: boolean; deleting: boolean; hasExistingPresentation: boolean }): string { if (params.loading || params.deleting) { return t('publish.publishing') } return params.hasExistingPresentation ? t('presentation.update.button') : t('publish.button') } 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(() => buildInitialDraft(existingPresentation, existingAuthorName)) const [validationError, setValidationError] = useState(null) const [deleting, setDeleting] = useState(false) // Update authorName when profile changes useEffect(() => { syncAuthorNameIntoDraft({ existingAuthorName, draftAuthorName: draft.authorName, hasExistingPresentation: Boolean(existingPresentation), setDraft }) }, [existingAuthorName, existingPresentation, draft.authorName]) const handleSubmit = useCallback( async (e: FormEvent) => { e.preventDefault() await submitPresentationDraft({ draft, setValidationError, publishPresentation }) }, [draft, publishPresentation] ) const handleDelete = useCallback(async () => { await deletePresentationFlow({ existingPresentationId: existingPresentation?.id, deletePresentation, router, setDeleting, setValidationError, }) }, [existingPresentation, deletePresentation, router]) return { loading, error, success, draft, setDraft, validationError, handleSubmit, deleting, handleDelete } } function buildInitialDraft(existingPresentation: Article | null | undefined, existingAuthorName: string | undefined): AuthorPresentationDraft { 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: '', } } function syncAuthorNameIntoDraft(params: { existingAuthorName: string | undefined draftAuthorName: string hasExistingPresentation: boolean setDraft: (updater: (prev: AuthorPresentationDraft) => AuthorPresentationDraft) => void }): void { if (!params.existingAuthorName || params.hasExistingPresentation || params.existingAuthorName === params.draftAuthorName) { return } params.setDraft((prev) => ({ ...prev, authorName: params.existingAuthorName as string })) } async function submitPresentationDraft(params: { draft: AuthorPresentationDraft setValidationError: (value: string | null) => void publishPresentation: (draft: AuthorPresentationDraft) => Promise }): Promise { const error = validatePresentationDraft(params.draft) if (error) { params.setValidationError(error) return } params.setValidationError(null) await params.publishPresentation(params.draft) } function validatePresentationDraft(draft: AuthorPresentationDraft): string | null { const address = draft.mainnetAddress.trim() if (!ADDRESS_PATTERN.test(address)) { return t('presentation.validation.invalidAddress') } if (!draft.authorName.trim()) { return t('presentation.validation.authorNameRequired') } return null } async function deletePresentationFlow(params: { existingPresentationId: string | undefined deletePresentation: (articleId: string) => Promise router: ReturnType setDeleting: (value: boolean) => void setValidationError: (value: string | null) => void }): Promise { if (!params.existingPresentationId) { return } const confirmed = await userConfirm(t('presentation.delete.confirm')) if (!confirmed) { return } params.setDeleting(true) params.setValidationError(null) try { await params.deletePresentation(params.existingPresentationId) await params.router.push('/') } catch (e) { params.setValidationError(e instanceof Error ? e.message : t('presentation.delete.error')) } finally { params.setDeleting(false) } } 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 = (): Promise => generateNoAccount({ setGenerating, setError, setRecoveryPhrase, setNpub, setShowRecoveryStep }) const handleRecoveryContinue = (): void => transitionToUnlock({ setShowRecoveryStep, setShowUnlockModal }) const handleUnlockSuccess = (): void => resetNoAccountAfterUnlock({ setShowUnlockModal, setRecoveryPhrase, setNpub }) const handleImportSuccess = (): void => { setShowImportModal(false) setShowUnlockModal(true) } return ( { void handleGenerate() }} onImport={() => setShowImportModal(true)} modals={ setShowImportModal(false)} onImportSuccess={handleImportSuccess} showRecoveryStep={showRecoveryStep} recoveryPhrase={recoveryPhrase} npub={npub} onRecoveryContinue={handleRecoveryContinue} showUnlockModal={showUnlockModal} onUnlockSuccess={handleUnlockSuccess} onCloseUnlock={() => setShowUnlockModal(false)} /> } /> ) } async function generateNoAccount(params: { setGenerating: (value: boolean) => void setError: (value: string | null) => void setRecoveryPhrase: (value: string[]) => void setNpub: (value: string) => void setShowRecoveryStep: (value: boolean) => void }): Promise { params.setGenerating(true) params.setError(null) try { const { nostrAuthService } = await import('@/lib/nostrAuth') const result = await nostrAuthService.createAccount() params.setRecoveryPhrase(result.recoveryPhrase) params.setNpub(result.npub) params.setShowRecoveryStep(true) } catch (e) { params.setError(e instanceof Error ? e.message : t('account.create.error.failed')) } finally { params.setGenerating(false) } } function transitionToUnlock(params: { setShowRecoveryStep: (value: boolean) => void; setShowUnlockModal: (value: boolean) => void }): void { params.setShowRecoveryStep(false) params.setShowUnlockModal(true) } function resetNoAccountAfterUnlock(params: { setShowUnlockModal: (value: boolean) => void setRecoveryPhrase: (value: string[]) => void setNpub: (value: string) => void }): void { params.setShowUnlockModal(false) params.setRecoveryPhrase([]) params.setNpub('') } function NoAccountCard(params: { error: string | null generating: boolean onGenerate: () => void onImport: () => void modals: React.ReactElement }): React.ReactElement { return (

Créez un compte ou importez votre clé secrète pour commencer

{params.error &&

{params.error}

} {params.generating &&

Génération du compte...

} {params.modals}
) } function NoAccountModals(params: { showImportModal: boolean onImportSuccess: () => void onCloseImport: () => void showRecoveryStep: boolean recoveryPhrase: string[] npub: string onRecoveryContinue: () => void showUnlockModal: boolean onUnlockSuccess: () => void onCloseUnlock: () => void }): React.ReactElement { return ( <> {params.showImportModal && } {params.showRecoveryStep && } {params.showUnlockModal && } ) } function AuthorPresentationFormView(props: { pubkey: string | null profile: { name?: string; pubkey: string } | null }): React.ReactElement { const { checkPresentationExists } = useAuthorPresentation(props.pubkey) const presentation = useExistingPresentation({ pubkey: props.pubkey, checkPresentationExists }) const state = useAuthorPresentationState(props.pubkey, props.profile?.name, presentation.existingPresentation) if (!props.pubkey) { return } if (presentation.loadingPresentation) { return } if (state.success) { return } return ( { void state.handleDelete() }} hasExistingPresentation={presentation.existingPresentation !== null} /> ) } function LoadingNotice(): React.ReactElement { return (

{t('common.loading')}

) } function useExistingPresentation(params: { pubkey: string | null checkPresentationExists: () => Promise
}): { existingPresentation: Article | null; loadingPresentation: boolean } { const [existingPresentation, setExistingPresentation] = useState
(null) const [loadingPresentation, setLoadingPresentation] = useState(true) const { pubkey, checkPresentationExists } = params useEffect(() => { void loadExistingPresentation({ pubkey, checkPresentationExists, setExistingPresentation, setLoadingPresentation }) }, [pubkey, checkPresentationExists]) return { existingPresentation, loadingPresentation } } async function loadExistingPresentation(params: { pubkey: string | null checkPresentationExists: () => Promise
setExistingPresentation: (value: Article | null) => void setLoadingPresentation: (value: boolean) => void }): Promise { if (!params.pubkey) { params.setLoadingPresentation(false) return } try { params.setExistingPresentation(await params.checkPresentationExists()) } catch (e) { console.error('Error loading presentation:', e) } finally { params.setLoadingPresentation(false) } } 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 }