Fix: profil image2
This commit is contained in:
parent
94ac35f309
commit
a058056475
@ -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)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
21
components/KeyIndicator.tsx
Normal file
21
components/KeyIndicator.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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 />
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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(),
|
||||||
},
|
},
|
||||||
|
|||||||
14
lib/nip95.ts
14
lib/nip95.ts
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user