**Motivations:** - Translate settings page and all its components to French and English - Provide consistent multilingual experience **Root causes:** - Settings page and components were hardcoded in English - No translation support for key management and NIP-95 configuration **Correctifs:** - None (new feature) **Evolutions:** - Added translations for settings page title - Added translations for KeyManagementManager component: - Public keys display (npub and hex) - Import form and validation messages - Recovery phrase display - All buttons and warnings - Added translations for Nip95ConfigManager component: - Endpoint list and management - Add/edit/remove actions - Error messages - Updated both fr.txt and en.txt translation files - All text now uses t() function for i18n support **Pages affectées:** - pages/settings.tsx - components/KeyManagementManager.tsx - components/Nip95ConfigManager.tsx - public/locales/fr.txt - public/locales/en.txt - locales/fr.txt - locales/en.txt
286 lines
10 KiB
TypeScript
286 lines
10 KiB
TypeScript
import { useState, useEffect } from 'react'
|
||
import { configStorage } from '@/lib/configStorage'
|
||
import type { Nip95Config } from '@/lib/configStorageTypes'
|
||
import { t } from '@/lib/i18n'
|
||
|
||
interface Nip95ConfigManagerProps {
|
||
onConfigChange?: () => void
|
||
}
|
||
|
||
export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps) {
|
||
const [apis, setApis] = useState<Nip95Config[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [editingId, setEditingId] = useState<string | null>(null)
|
||
const [newUrl, setNewUrl] = useState('')
|
||
const [showAddForm, setShowAddForm] = useState(false)
|
||
|
||
useEffect(() => {
|
||
void loadApis()
|
||
}, [])
|
||
|
||
async function loadApis() {
|
||
try {
|
||
setLoading(true)
|
||
setError(null)
|
||
const config = await configStorage.getConfig()
|
||
setApis(config.nip95Apis.sort((a, b) => a.priority - b.priority))
|
||
} catch (e) {
|
||
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.loadFailed')
|
||
setError(errorMessage)
|
||
console.error('Error loading NIP-95 APIs:', e)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
async function handleToggleEnabled(id: string, enabled: boolean) {
|
||
try {
|
||
await configStorage.updateNip95Api(id, { enabled })
|
||
await loadApis()
|
||
onConfigChange?.()
|
||
} catch (e) {
|
||
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.updateFailed')
|
||
setError(errorMessage)
|
||
console.error('Error updating NIP-95 API:', e)
|
||
}
|
||
}
|
||
|
||
async function handleUpdatePriority(id: string, priority: number) {
|
||
try {
|
||
await configStorage.updateNip95Api(id, { priority })
|
||
await loadApis()
|
||
onConfigChange?.()
|
||
} catch (e) {
|
||
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.priorityFailed')
|
||
setError(errorMessage)
|
||
console.error('Error updating priority:', e)
|
||
}
|
||
}
|
||
|
||
async function handleUpdateUrl(id: string, url: string) {
|
||
try {
|
||
await configStorage.updateNip95Api(id, { url })
|
||
await loadApis()
|
||
setEditingId(null)
|
||
onConfigChange?.()
|
||
} catch (e) {
|
||
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.urlFailed')
|
||
setError(errorMessage)
|
||
console.error('Error updating URL:', e)
|
||
}
|
||
}
|
||
|
||
async function handleAddApi() {
|
||
if (!newUrl.trim()) {
|
||
setError(t('settings.nip95.error.urlRequired'))
|
||
return
|
||
}
|
||
|
||
try {
|
||
// Validate URL format
|
||
new URL(newUrl)
|
||
await configStorage.addNip95Api(newUrl.trim(), false)
|
||
setNewUrl('')
|
||
setShowAddForm(false)
|
||
await loadApis()
|
||
onConfigChange?.()
|
||
} catch (e) {
|
||
if (e instanceof TypeError && e.message.includes('Invalid URL')) {
|
||
setError(t('settings.nip95.error.invalidUrl'))
|
||
} else {
|
||
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.addFailed')
|
||
setError(errorMessage)
|
||
}
|
||
console.error('Error adding NIP-95 API:', e)
|
||
}
|
||
}
|
||
|
||
async function handleRemoveApi(id: string) {
|
||
if (!confirm(t('settings.nip95.remove.confirm'))) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
await configStorage.removeNip95Api(id)
|
||
await loadApis()
|
||
onConfigChange?.()
|
||
} catch (e) {
|
||
const errorMessage = e instanceof Error ? e.message : t('settings.nip95.error.removeFailed')
|
||
setError(errorMessage)
|
||
console.error('Error removing NIP-95 API:', e)
|
||
}
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="text-center py-8 text-neon-cyan">
|
||
<div>{t('settings.nip95.loading')}</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{error && (
|
||
<div className="bg-red-900/30 border border-red-500/50 rounded p-4 text-red-300">
|
||
{error}
|
||
<button
|
||
onClick={() => setError(null)}
|
||
className="ml-4 text-red-400 hover:text-red-200"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex justify-between items-center">
|
||
<h2 className="text-2xl font-bold text-neon-cyan">{t('settings.nip95.title')}</h2>
|
||
<button
|
||
onClick={() => setShowAddForm(!showAddForm)}
|
||
className="px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan border border-neon-cyan/50 rounded transition-colors"
|
||
>
|
||
{showAddForm ? t('settings.nip95.add.cancel') : t('settings.nip95.addButton')}
|
||
</button>
|
||
</div>
|
||
|
||
{showAddForm && (
|
||
<div className="bg-cyber-dark border border-neon-cyan/30 rounded p-4 space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-cyber-accent mb-2">
|
||
{t('settings.nip95.add.url')}
|
||
</label>
|
||
<input
|
||
type="url"
|
||
value={newUrl}
|
||
onChange={(e) => setNewUrl(e.target.value)}
|
||
placeholder={t('settings.nip95.add.placeholder')}
|
||
className="w-full px-4 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
|
||
/>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={() => void handleAddApi()}
|
||
className="px-4 py-2 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan border border-neon-cyan/50 rounded transition-colors"
|
||
>
|
||
{t('settings.nip95.add.add')}
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setShowAddForm(false)
|
||
setNewUrl('')
|
||
setError(null)
|
||
}}
|
||
className="px-4 py-2 bg-cyber-accent/20 hover:bg-cyber-accent/30 text-cyber-accent border border-cyber-accent/50 rounded transition-colors"
|
||
>
|
||
{t('settings.nip95.add.cancel')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-4">
|
||
{apis.length === 0 ? (
|
||
<div className="text-center py-8 text-cyber-accent">
|
||
{t('settings.nip95.empty')}
|
||
</div>
|
||
) : (
|
||
apis.map((api) => (
|
||
<div
|
||
key={api.id}
|
||
className="bg-cyber-dark border border-neon-cyan/30 rounded p-4 space-y-3"
|
||
>
|
||
<div className="flex items-start justify-between gap-4">
|
||
<div className="flex-1">
|
||
{editingId === api.id ? (
|
||
<div className="space-y-2">
|
||
<input
|
||
type="url"
|
||
defaultValue={api.url}
|
||
onBlur={(e) => {
|
||
if (e.target.value !== api.url) {
|
||
void handleUpdateUrl(api.id, e.target.value)
|
||
} else {
|
||
setEditingId(null)
|
||
}
|
||
}}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') {
|
||
e.currentTarget.blur()
|
||
} else if (e.key === 'Escape') {
|
||
setEditingId(null)
|
||
}
|
||
}}
|
||
className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/50 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
|
||
autoFocus
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div
|
||
className="text-neon-cyan cursor-pointer hover:text-neon-green transition-colors"
|
||
onClick={() => setEditingId(api.id)}
|
||
title="Click to edit URL"
|
||
>
|
||
{api.url}
|
||
</div>
|
||
)}
|
||
<div className="text-sm text-cyber-accent mt-1">
|
||
Priority: {api.priority} | ID: {api.id}
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<label className="flex items-center gap-2 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={api.enabled}
|
||
onChange={(e) => void handleToggleEnabled(api.id, e.target.checked)}
|
||
className="w-4 h-4 text-neon-cyan bg-cyber-darker border-cyber-accent rounded focus:ring-neon-cyan"
|
||
/>
|
||
<span className="text-sm text-cyber-accent">
|
||
{api.enabled ? t('settings.nip95.list.enabled') : t('settings.nip95.list.disabled')}
|
||
</span>
|
||
</label>
|
||
<button
|
||
onClick={() => void handleRemoveApi(api.id)}
|
||
className="px-3 py-1 text-sm bg-red-900/30 hover:bg-red-900/50 text-red-300 border border-red-500/50 rounded transition-colors"
|
||
title={t('settings.nip95.list.remove')}
|
||
>
|
||
{t('settings.nip95.list.remove')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-4">
|
||
<label className="flex items-center gap-2">
|
||
<span className="text-sm text-cyber-accent">{t('settings.nip95.list.priority')}:</span>
|
||
<input
|
||
type="number"
|
||
min="1"
|
||
value={api.priority}
|
||
onChange={(e) => {
|
||
const priority = parseInt(e.target.value, 10)
|
||
if (!isNaN(priority) && priority > 0) {
|
||
void handleUpdatePriority(api.id, priority)
|
||
}
|
||
}}
|
||
className="w-20 px-2 py-1 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none"
|
||
/>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
<div className="text-sm text-cyber-accent space-y-2">
|
||
<p>
|
||
<strong>Note:</strong> Endpoints are tried in priority order (lower number = higher priority).
|
||
Only enabled endpoints will be used for uploads.
|
||
</p>
|
||
<p>
|
||
If an endpoint fails, the next enabled endpoint will be tried automatically.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|