story-research-zapwall/components/Nip95ConfigManager.tsx

285 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react'
import { configStorage } from '@/lib/configStorage'
import type { Nip95Config } from '@/lib/configStorageTypes'
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 : 'Failed to load NIP-95 APIs'
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 : 'Failed to update API'
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 : 'Failed to update priority'
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 : 'Failed to update URL'
setError(errorMessage)
console.error('Error updating URL:', e)
}
}
async function handleAddApi() {
if (!newUrl.trim()) {
setError('URL is required')
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('Invalid URL format')
} else {
const errorMessage = e instanceof Error ? e.message : 'Failed to add API'
setError(errorMessage)
}
console.error('Error adding NIP-95 API:', e)
}
}
async function handleRemoveApi(id: string) {
if (!confirm('Are you sure you want to remove this endpoint?')) {
return
}
try {
await configStorage.removeNip95Api(id)
await loadApis()
onConfigChange?.()
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to remove API'
setError(errorMessage)
console.error('Error removing NIP-95 API:', e)
}
}
if (loading) {
return (
<div className="text-center py-8 text-neon-cyan">
<div>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">NIP-95 Upload Endpoints</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 ? 'Cancel' : '+ Add Endpoint'}
</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">
Endpoint URL
</label>
<input
type="url"
value={newUrl}
onChange={(e) => setNewUrl(e.target.value)}
placeholder="https://example.com/upload"
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"
>
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"
>
Cancel
</button>
</div>
</div>
)}
<div className="space-y-4">
{apis.length === 0 ? (
<div className="text-center py-8 text-cyber-accent">
No NIP-95 endpoints configured
</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 ? 'Enabled' : '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="Remove endpoint"
>
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">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>
)
}