Fix: profil image2

This commit is contained in:
Nicolas Cantu 2026-01-05 23:07:12 +01:00
parent 94ac35f309
commit a058056475
7 changed files with 113 additions and 23 deletions

View File

@ -2,6 +2,7 @@ import { useState } from 'react'
import { uploadNip95Media } from '@/lib/nip95' import { uploadNip95Media } from '@/lib/nip95'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import Image from 'next/image' import Image from 'next/image'
import { UnlockAccountModal } from './UnlockAccountModal'
interface ImageUploadFieldProps { interface ImageUploadFieldProps {
id: string id: string
@ -94,6 +95,8 @@ async function processFileUpload(file: File, onChange: (url: string) => void, se
function useImageUpload(onChange: (url: string) => void) { function useImageUpload(onChange: (url: string) => void) {
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [showUnlockModal, setShowUnlockModal] = useState(false)
const [pendingFile, setPendingFile] = useState<File | null>(null)
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] const file = e.target.files?.[0]
@ -107,35 +110,70 @@ function useImageUpload(onChange: (url: string) => void) {
try { try {
await processFileUpload(file, onChange, setError) await processFileUpload(file, onChange, setError)
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : t('presentation.field.picture.error.uploadFailed')) const error = e instanceof Error ? e : new Error(String(e))
// Check if unlock is required
if (error.message === 'UNLOCK_REQUIRED' || (error as any).unlockRequired) {
setPendingFile(file)
setShowUnlockModal(true)
setError(null) // Don't show error, show unlock modal instead
} else {
setError(error.message || t('presentation.field.picture.error.uploadFailed'))
}
} finally { } finally {
setUploading(false) setUploading(false)
} }
} }
return { uploading, error, handleFileSelect } const handleUnlockSuccess = async () => {
setShowUnlockModal(false)
if (pendingFile) {
// Retry upload after unlock
setUploading(true)
setError(null)
try {
await processFileUpload(pendingFile, onChange, setError)
setPendingFile(null)
} catch (e) {
setError(e instanceof Error ? e.message : t('presentation.field.picture.error.uploadFailed'))
} finally {
setUploading(false)
}
}
}
return { uploading, error, handleFileSelect, showUnlockModal, setShowUnlockModal, handleUnlockSuccess }
} }
export function ImageUploadField({ id, label, value, onChange, helpText }: ImageUploadFieldProps) { export function ImageUploadField({ id, label, value, onChange, helpText }: ImageUploadFieldProps) {
const { uploading, error, handleFileSelect } = useImageUpload(onChange) const { uploading, error, handleFileSelect, showUnlockModal, setShowUnlockModal, handleUnlockSuccess } = useImageUpload(onChange)
const displayLabel = label ?? t('presentation.field.picture') const displayLabel = label ?? t('presentation.field.picture')
const displayHelpText = helpText ?? t('presentation.field.picture.help') const displayHelpText = helpText ?? t('presentation.field.picture.help')
return ( return (
<div className="space-y-2"> <>
<label htmlFor={id} className="block text-sm font-medium text-neon-cyan"> <div className="space-y-2">
{displayLabel} <label htmlFor={id} className="block text-sm font-medium text-neon-cyan">
</label> {displayLabel}
{value && <ImagePreview value={value} />} </label>
<ImageUploadControls {value && <ImagePreview value={value} />}
id={id} <ImageUploadControls
uploading={uploading} id={id}
value={value} uploading={uploading}
onChange={onChange} value={value}
onFileSelect={handleFileSelect} onChange={onChange}
/> onFileSelect={handleFileSelect}
{error && <p className="text-sm text-red-400">{error}</p>} />
{displayHelpText && <p className="text-sm text-cyber-accent">{displayHelpText}</p>} {error && <p className="text-sm text-red-400">{error}</p>}
</div> {displayHelpText && <p className="text-sm text-cyber-accent">{displayHelpText}</p>}
</div>
{showUnlockModal && (
<UnlockAccountModal
onSuccess={handleUnlockSuccess}
onClose={() => {
setShowUnlockModal(false)
}}
/>
)}
</>
) )
} }

View File

@ -0,0 +1,21 @@
import { useNostrAuth } from '@/hooks/useNostrAuth'
export function KeyIndicator() {
const { pubkey, isUnlocked } = useNostrAuth()
// No indicator if no account
if (!pubkey) {
return null
}
// Red if private key is accessible (unlocked)
// Green if only public key is accessible (connected but not unlocked)
const color = isUnlocked ? 'text-red-500' : 'text-green-500'
const title = isUnlocked ? 'Private key accessible' : 'Public key accessible'
return (
<span className={`ml-2 text-xl ${color}`} title={title}>
🔑
</span>
)
}

View File

@ -2,13 +2,15 @@ import Link from 'next/link'
import { ConditionalPublishButton } from './ConditionalPublishButton' import { ConditionalPublishButton } from './ConditionalPublishButton'
import { LanguageSelector } from './LanguageSelector' import { LanguageSelector } from './LanguageSelector'
import { t } from '@/lib/i18n' import { t } from '@/lib/i18n'
import { KeyIndicator } from './KeyIndicator'
export function PageHeader() { export function PageHeader() {
return ( return (
<header className="bg-cyber-dark border-b border-neon-cyan/30 shadow-glow-cyan"> <header className="bg-cyber-dark border-b border-neon-cyan/30 shadow-glow-cyan">
<div className="max-w-4xl mx-auto px-4 py-4 flex justify-between items-center"> <div className="max-w-4xl mx-auto px-4 py-4 flex justify-between items-center">
<Link href="/" className="text-2xl font-bold text-neon-cyan text-glow-cyan font-mono hover:text-neon-green transition-colors"> <Link href="/" className="text-2xl font-bold text-neon-cyan text-glow-cyan font-mono hover:text-neon-green transition-colors flex items-center">
{t('home.title')} {t('home.title')}
<KeyIndicator />
</Link> </Link>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<LanguageSelector /> <LanguageSelector />

View File

@ -1,11 +1,15 @@
import { ConnectButton } from '@/components/ConnectButton' import { ConnectButton } from '@/components/ConnectButton'
import { ConditionalPublishButton } from './ConditionalPublishButton' import { ConditionalPublishButton } from './ConditionalPublishButton'
import { KeyIndicator } from './KeyIndicator'
export function ProfileHeader() { export function ProfileHeader() {
return ( return (
<header className="bg-white shadow-sm"> <header className="bg-white shadow-sm">
<div className="max-w-4xl mx-auto px-4 py-4 flex justify-between items-center"> <div className="max-w-4xl mx-auto px-4 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-900">zapwall.fr</h1> <h1 className="text-2xl font-bold text-gray-900 flex items-center">
zapwall.fr
<KeyIndicator />
</h1>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<ConditionalPublishButton /> <ConditionalPublishButton />
<ConnectButton /> <ConnectButton />

View File

@ -42,7 +42,7 @@ export const DEFAULT_NIP95_APIS: Nip95Config[] = [
{ {
id: 'nostrimg', id: 'nostrimg',
url: 'https://nostrimg.com/api/upload', url: 'https://nostrimg.com/api/upload',
enabled: true, // Temporarily enabled for diagnostic logging - disable if 500 errors persist enabled: true,
priority: 1, priority: 1,
createdAt: Date.now(), createdAt: Date.now(),
}, },

View File

@ -137,12 +137,17 @@ export async function uploadNip95Media(file: File): Promise<MediaRef> {
const isUnlocked = nostrAuthService.isUnlocked() const isUnlocked = nostrAuthService.isUnlocked()
if (!pubkey) { if (!pubkey) {
console.warn('NIP-98 authentication required for nostrcheck.me but no account found. Please create or import an account.') console.warn('NIP-98 authentication required for nostrcheck.me but no account found. Please create or import an account.')
continue
} else if (!isUnlocked) { } else if (!isUnlocked) {
console.warn('NIP-98 authentication required for nostrcheck.me but account is not unlocked. Please unlock your account with your recovery phrase to use this endpoint.') // Throw a special error that can be caught to trigger unlock modal
// This error should propagate to the caller, not be caught here
const unlockError = new Error('UNLOCK_REQUIRED')
;(unlockError as any).unlockRequired = true
throw unlockError
} else { } else {
console.warn('NIP-98 authentication required for nostrcheck.me but not available. Skipping endpoint.') console.warn('NIP-98 authentication required for nostrcheck.me but not available. Skipping endpoint.')
continue
} }
continue
} }
try { try {
// Generate NIP-98 token for the actual endpoint (not the proxy) // Generate NIP-98 token for the actual endpoint (not the proxy)
@ -170,6 +175,11 @@ export async function uploadNip95Media(file: File): Promise<MediaRef> {
const error = e instanceof Error ? e : new Error(String(e)) const error = e instanceof Error ? e : new Error(String(e))
const errorMessage = error.message const errorMessage = error.message
// If unlock is required, propagate the error immediately
if (errorMessage === 'UNLOCK_REQUIRED' || (error as any).unlockRequired) {
throw error
}
console.error('NIP-95 upload endpoint error:', { console.error('NIP-95 upload endpoint error:', {
endpoint, endpoint,
error: errorMessage, error: errorMessage,

View File

@ -209,6 +209,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}) })
requestFormData.on('error', (error) => { requestFormData.on('error', (error) => {
console.error('NIP-95 proxy FormData error:', {
targetEndpoint,
hostname: url.hostname,
error: error instanceof Error ? error.message : 'Unknown FormData error',
})
reject(error)
})
fileStream.on('error', (error) => {
console.error('NIP-95 proxy file stream error:', {
targetEndpoint,
hostname: url.hostname,
filepath: fileField.filepath,
error: error instanceof Error ? error.message : 'Unknown file stream error',
})
reject(error) reject(error)
}) })