added nftcache functionality and cleaned up the settings page

This commit is contained in:
Siavash Sameni
2025-08-29 09:05:58 +04:00
parent 62653437b5
commit 4c67bf3a15
2 changed files with 350 additions and 197 deletions

View File

@@ -40,6 +40,11 @@ export default function SettingsModal({ open, initial, onClose, onSave }: Settin
const [rpcBase, setRpcBase] = useState<string>("");
const [rpcArbitrum, setRpcArbitrum] = useState<string>("");
const [rpcMainnet, setRpcMainnet] = useState<string>("");
// NFTCache settings (stored in localStorage)
const ENV_NFTCACHE = (process.env.NEXT_PUBLIC_NFTCACHE_URL || process.env.NFTCACHE_URL || '/nftcache').replace(/\/$/, '');
const [nftcacheEnabled, setNftcacheEnabled] = useState<boolean>(false);
const [nftcacheBaseUrl, setNftcacheBaseUrl] = useState<string>(ENV_NFTCACHE);
const [nftcacheApiKey, setNftcacheApiKey] = useState<string>('');
useEffect(() => {
if (open) {
@@ -58,6 +63,13 @@ export default function SettingsModal({ open, initial, onClose, onSave }: Settin
setRpcBase(b || 'https://base.llamarpc.com');
setRpcArbitrum(a || '');
setRpcMainnet(m || '');
// Load NFTCache settings
const nEn = ls?.getItem('nftcache:enabled');
const nUrl = ls?.getItem('nftcache:baseUrl');
const nKey = ls?.getItem('nftcache:apiKey');
setNftcacheEnabled(nEn === '1');
setNftcacheBaseUrl((nUrl && nUrl.trim()) || ENV_NFTCACHE);
setNftcacheApiKey(nKey || (process.env.NEXT_PUBLIC_NFTCACHE_API_KEY || process.env.NFTCACHE_API_KEY || ''));
} catch {}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -118,35 +130,91 @@ export default function SettingsModal({ open, initial, onClose, onSave }: Settin
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
<div className="relative z-10 w-full max-w-xl rounded-lg border border-gray-700 bg-gray-900 p-4 text-gray-100 shadow-lg">
<div className="text-lg font-semibold">Notification Settings</div>
<p className="text-sm text-gray-300">Configure your notification provider and scheduler credentials.</p>
<div className="mt-3 grid grid-cols-2 gap-3 text-sm">
<label className="col-span-2">
<span className="text-gray-300">Provider</span>
<select
className="mt-1 w-full rounded-none border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100 appearance-none shadow-none focus:outline-none focus:ring-0"
value={provider}
onChange={(e) => setForm((f) => ({ ...f, provider: e.target.value as NotificationProvider }))}
>
<option value="">Select</option>
<option value="ntfy">ntfy.sh</option>
<option value="gotify">Gotify</option>
<option value="sns">Amazon SNS</option>
</select>
</label>
<p className="text-sm text-gray-300">Configure your notification provider, scheduler, RPC and NFTCache.</p>
<label className="col-span-2">
<span className="text-gray-300">Scheduler</span>
<select
className="mt-1 w-full rounded-none border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100 appearance-none shadow-none focus:outline-none focus:ring-0"
value={scheduler}
onChange={(e) => setForm((f) => ({ ...f, scheduler: e.target.value as SchedulerProvider }))}
>
<option value="schedy">Schedy</option>
</select>
</label>
{/* Section: Basics */}
<details className="mt-3" open>
<summary className="cursor-pointer select-none text-sm font-semibold text-gray-200">Basics</summary>
<div className="mt-2 grid grid-cols-2 gap-3 text-sm">
<label className="col-span-2">
<span className="text-gray-300">Provider</span>
<select
className="mt-1 w-full rounded-none border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100 appearance-none shadow-none focus:outline-none focus:ring-0"
value={provider}
onChange={(e) => setForm((f) => ({ ...f, provider: e.target.value as NotificationProvider }))}
>
<option value="">Select</option>
<option value="ntfy">ntfy.sh</option>
<option value="gotify">Gotify</option>
<option value="sns">Amazon SNS</option>
</select>
</label>
<label className="col-span-2">
<span className="text-gray-300">Scheduler</span>
<select
className="mt-1 w-full rounded-none border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100 appearance-none shadow-none focus:outline-none focus:ring-0"
value={scheduler}
onChange={(e) => setForm((f) => ({ ...f, scheduler: e.target.value as SchedulerProvider }))}
>
<option value="schedy">Schedy</option>
</select>
</label>
</div>
</details>
{/* Section: Email & Timing */}
<details className="mt-3" open>
<summary className="cursor-pointer select-none text-sm font-semibold text-gray-200">Email & Timing</summary>
<div className="mt-2 grid grid-cols-2 gap-3 text-sm">
<label>
<span className="text-gray-300">Email to notify</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="you@example.com"
value={form.email || ''}
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">Backup Email (optional)</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="backup@example.com"
value={form.backupEmail || ''}
onChange={(e) => setForm((f) => ({ ...f, backupEmail: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">Days before liquidation</span>
<input
type="number"
min={0}
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
value={form.daysBefore ?? 10}
onChange={(e) => setForm((f) => ({ ...f, daysBefore: Number(e.target.value) }))}
/>
</label>
<label>
<span className="text-gray-300">Backup delay (days after first email)</span>
<input
type="number"
min={1}
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
value={form.backupDelayDays ?? 1}
onChange={(e) => setForm((f) => ({ ...f, backupDelayDays: Number(e.target.value) }))}
/>
</label>
</div>
</details>
{/* Section: Scheduler (Schedy) */}
<details className="mt-3">
<summary className="cursor-pointer select-none text-sm font-semibold text-gray-200">Scheduler</summary>
{scheduler === 'schedy' && (
<>
<div className="mt-2 grid grid-cols-2 gap-3 text-sm">
<label>
<span className="text-gray-300">Schedy Base URL</span>
<input
@@ -165,175 +233,177 @@ export default function SettingsModal({ open, initial, onClose, onSave }: Settin
onChange={(e) => setForm((f) => ({ ...f, schedyApiKey: e.target.value }))}
/>
</label>
</>
)}
<label>
<span className="text-gray-300">Email to notify</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="you@example.com"
value={form.email || ''}
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">Backup Email (optional)</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="backup@example.com"
value={form.backupEmail || ''}
onChange={(e) => setForm((f) => ({ ...f, backupEmail: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">Days before liquidation</span>
<input
type="number"
min={0}
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
value={form.daysBefore ?? 10}
onChange={(e) => setForm((f) => ({ ...f, daysBefore: Number(e.target.value) }))}
/>
</label>
<label>
<span className="text-gray-300">Backup delay (days after first email)</span>
<input
type="number"
min={1}
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
value={form.backupDelayDays ?? 1}
onChange={(e) => setForm((f) => ({ ...f, backupDelayDays: Number(e.target.value) }))}
/>
</label>
{provider === 'ntfy' && (
<>
<label>
<span className="text-gray-300">ntfy Server</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="https://ntfy.sh"
value={form.ntfyServer || ''}
onChange={(e) => setForm((f) => ({ ...f, ntfyServer: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">ntfy Topic</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="topic-name"
value={form.ntfyTopic || ''}
onChange={(e) => setForm((f) => ({ ...f, ntfyTopic: e.target.value }))}
/>
</label>
</>
)}
{provider === 'gotify' && (
<>
<label>
<span className="text-gray-300">Gotify Server</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="https://gotify.example.com"
value={form.gotifyServer || ''}
onChange={(e) => setForm((f) => ({ ...f, gotifyServer: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">Gotify App Token</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="token"
value={form.gotifyToken || ''}
onChange={(e) => setForm((f) => ({ ...f, gotifyToken: e.target.value }))}
/>
</label>
</>
)}
{provider === 'sns' && (
<>
<label>
<span className="text-gray-300">AWS Region</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="us-east-1"
value={form.snsRegion || ''}
onChange={(e) => setForm((f) => ({ ...f, snsRegion: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">SNS Topic ARN</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="arn:aws:sns:..."
value={form.snsTopicArn || ''}
onChange={(e) => setForm((f) => ({ ...f, snsTopicArn: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">AWS Access Key ID</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="AKIA..."
value={form.snsAccessKeyId || ''}
onChange={(e) => setForm((f) => ({ ...f, snsAccessKeyId: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">AWS Secret Access Key</span>
<input
type="password"
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="••••••••"
value={form.snsSecretAccessKey || ''}
onChange={(e) => setForm((f) => ({ ...f, snsSecretAccessKey: e.target.value }))}
/>
</label>
<div className="col-span-2 text-xs text-amber-300">
Storing AWS credentials in the browser is insecure. Prefer a server-side relay.
</div>
</>
)}
{/* RPC settings */}
<div className="col-span-2 mt-2 border-t border-gray-700 pt-2">
<div className="text-sm font-semibold text-gray-200 mb-1">RPC endpoints (per network)</div>
<div className="grid grid-cols-2 gap-3">
<label>
<span className="text-gray-300">Base RPC (default https://base.llamarpc.com)</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="https://base.llamarpc.com"
value={rpcBase}
onChange={(e) => setRpcBase(e.target.value)}
/>
</label>
<label>
<span className="text-gray-300">Arbitrum RPC (optional)</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="https://arb.yourrpc.example"
value={rpcArbitrum}
onChange={(e) => setRpcArbitrum(e.target.value)}
/>
</label>
<label className="col-span-2">
<span className="text-gray-300">Mainnet RPC (optional)</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="https://mainnet.yourrpc.example"
value={rpcMainnet}
onChange={(e) => setRpcMainnet(e.target.value)}
/>
</label>
<div className="col-span-2 text-xs text-gray-400">Changes apply immediately after Save without rebuild.</div>
</div>
)}
</details>
{/* Section: Provider-specific */}
<details className="mt-3">
<summary className="cursor-pointer select-none text-sm font-semibold text-gray-200">Provider Settings</summary>
<div className="mt-2 grid grid-cols-2 gap-3 text-sm">
{provider === 'ntfy' && (
<>
<label>
<span className="text-gray-300">ntfy Server</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="https://ntfy.sh"
value={form.ntfyServer || ''}
onChange={(e) => setForm((f) => ({ ...f, ntfyServer: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">ntfy Topic</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="topic-name"
value={form.ntfyTopic || ''}
onChange={(e) => setForm((f) => ({ ...f, ntfyTopic: e.target.value }))}
/>
</label>
</>
)}
{provider === 'gotify' && (
<>
<label>
<span className="text-gray-300">Gotify Server</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="https://gotify.example.com"
value={form.gotifyServer || ''}
onChange={(e) => setForm((f) => ({ ...f, gotifyServer: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">Gotify App Token</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="token"
value={form.gotifyToken || ''}
onChange={(e) => setForm((f) => ({ ...f, gotifyToken: e.target.value }))}
/>
</label>
</>
)}
{provider === 'sns' && (
<>
<label>
<span className="text-gray-300">AWS Region</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="us-east-1"
value={form.snsRegion || ''}
onChange={(e) => setForm((f) => ({ ...f, snsRegion: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">SNS Topic ARN</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="arn:aws:sns:..."
value={form.snsTopicArn || ''}
onChange={(e) => setForm((f) => ({ ...f, snsTopicArn: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">AWS Access Key ID</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="AKIA..."
value={form.snsAccessKeyId || ''}
onChange={(e) => setForm((f) => ({ ...f, snsAccessKeyId: e.target.value }))}
/>
</label>
<label>
<span className="text-gray-300">AWS Secret Access Key</span>
<input
type="password"
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="••••••••"
value={form.snsSecretAccessKey || ''}
onChange={(e) => setForm((f) => ({ ...f, snsSecretAccessKey: e.target.value }))}
/>
</label>
<div className="col-span-2 text-xs text-amber-300">
Storing AWS credentials in the browser is insecure. Prefer a server-side relay.
</div>
</>
)}
</div>
</div>
</details>
{/* Section: RPC endpoints */}
<details className="mt-3">
<summary className="cursor-pointer select-none text-sm font-semibold text-gray-200">RPC endpoints</summary>
<div className="mt-2 grid grid-cols-2 gap-3 text-sm">
<label>
<span className="text-gray-300">Base RPC (default https://base.llamarpc.com)</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="https://base.llamarpc.com"
value={rpcBase}
onChange={(e) => setRpcBase(e.target.value)}
/>
</label>
<label>
<span className="text-gray-300">Arbitrum RPC (optional)</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="https://arb.yourrpc.example"
value={rpcArbitrum}
onChange={(e) => setRpcArbitrum(e.target.value)}
/>
</label>
<label className="col-span-2">
<span className="text-gray-300">Mainnet RPC (optional)</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="https://mainnet.yourrpc.example"
value={rpcMainnet}
onChange={(e) => setRpcMainnet(e.target.value)}
/>
</label>
<div className="col-span-2 text-xs text-gray-400">Changes apply immediately after Save without rebuild.</div>
</div>
</details>
{/* Section: NFTCache */}
<details className="mt-3">
<summary className="cursor-pointer select-none text-sm font-semibold text-gray-200">NFTCache</summary>
<div className="mt-2 grid grid-cols-2 gap-3 text-sm">
<label className="flex items-center gap-2">
<input
type="checkbox"
className="h-4 w-4"
checked={nftcacheEnabled}
onChange={(e) => setNftcacheEnabled(e.target.checked)}
/>
<span className="text-gray-300">Enable NFTCache</span>
</label>
<label className="col-span-2">
<span className="text-gray-300">NFTCache Base URL</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="/nftcache"
value={nftcacheBaseUrl}
onChange={(e) => setNftcacheBaseUrl(e.target.value)}
/>
<div className="text-xs text-gray-400 mt-1">Proxied default via nginx: /nftcache. Endpoint used in-app: {nftcacheBaseUrl || ENV_NFTCACHE}/nfts</div>
</label>
<label className="col-span-2">
<span className="text-gray-300">NFTCache API Key (X-API-Key)</span>
<input
className="mt-1 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-gray-100"
placeholder="optional, can also be set via NEXT_PUBLIC_NFTCACHE_API_KEY"
value={nftcacheApiKey}
onChange={(e) => setNftcacheApiKey(e.target.value)}
/>
<div className="text-xs text-gray-400 mt-1">If empty, the app will try NEXT_PUBLIC_NFTCACHE_API_KEY from the environment.</div>
</label>
</div>
</details>
<div className="mt-4 flex justify-end gap-2">
<button className="rounded bg-gray-700 px-3 py-1.5" onClick={onClose}>Cancel</button>
@@ -367,6 +437,12 @@ export default function SettingsModal({ open, initial, onClose, onSave }: Settin
if (b) ls.setItem('rpc:base', b); else ls.removeItem('rpc:base');
if (a) ls.setItem('rpc:arbitrum', a); else ls.removeItem('rpc:arbitrum');
if (m) ls.setItem('rpc:mainnet', m); else ls.removeItem('rpc:mainnet');
// Persist NFTCache settings
ls.setItem('nftcache:enabled', nftcacheEnabled ? '1' : '0');
const nurl = (nftcacheBaseUrl || '').trim();
if (nurl) ls.setItem('nftcache:baseUrl', nurl); else ls.removeItem('nftcache:baseUrl');
const nkey = (nftcacheApiKey || '').trim();
if (nkey) ls.setItem('nftcache:apiKey', nkey); else ls.removeItem('nftcache:apiKey');
}
} catch {}
onSave(form);