created a new branch with alert functionality and added the compose files etc ..
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useWeb3Modal } from '@web3modal/wagmi/react';
|
||||
import { useAccount, useDisconnect } from 'wagmi';
|
||||
import { useAccount } from 'wagmi';
|
||||
import { formatAddress } from '@/utils/format';
|
||||
|
||||
export function ConnectButton() {
|
||||
const { open } = useWeb3Modal();
|
||||
const { address, isConnected } = useAccount();
|
||||
const { disconnect } = useDisconnect();
|
||||
|
||||
const handleConnect = () => {
|
||||
open();
|
||||
try {
|
||||
// Minimal connect without WalletConnect modal
|
||||
if (typeof window !== 'undefined' && (window as any).ethereum?.request) {
|
||||
(window as any).ethereum.request({ method: 'eth_requestAccounts' });
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
if (isConnected && address) {
|
||||
@@ -19,12 +21,7 @@ export function ConnectButton() {
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{formatAddress(address)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => disconnect()}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
{/* Disconnect can be done from the wallet UI; no WalletConnect here */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ConnectButton } from '@/components/ConnectButton';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'DApp', href: '/dapp', current: false },
|
||||
{ name: 'Settings', href: '/dapp?settings=1', current: false },
|
||||
{ name: 'README', href: 'https://git.manko.yoga/manawenuz/mortgagefi-helper', current: false, external: true },
|
||||
];
|
||||
|
||||
|
||||
277
components/SettingsModal.tsx
Normal file
277
components/SettingsModal.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
"use client";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { NotificationSettings, NotificationProvider, SchedulerProvider } from "@/types/notifications";
|
||||
import { scheduleTestNotification } from "@/utils/scheduler";
|
||||
|
||||
export interface SettingsModalProps {
|
||||
open: boolean;
|
||||
initial?: NotificationSettings;
|
||||
onClose: () => void;
|
||||
onSave: (settings: NotificationSettings) => void;
|
||||
}
|
||||
|
||||
const ENV_NTFY = (process.env.NEXT_PUBLIC_NTFY_URL || process.env.NTFY_URL || 'https://ntfy.sh').replace(/\/$/, '');
|
||||
const ENV_SCHEDY = (process.env.NEXT_PUBLIC_SCHEDY_URL || process.env.SCHEDY_URL || 'http://localhost:8080').replace(/\/$/, '');
|
||||
const ENV_SCHEDY_API_KEY = (process.env.NEXT_PUBLIC_SCHEDY_API_KEY || process.env.SCHEDY_API_KEY || '').trim();
|
||||
|
||||
const defaultSettings: NotificationSettings = {
|
||||
provider: "",
|
||||
scheduler: 'schedy',
|
||||
ntfyServer: ENV_NTFY,
|
||||
ntfyTopic: "",
|
||||
gotifyServer: "",
|
||||
gotifyToken: "",
|
||||
snsRegion: "",
|
||||
snsTopicArn: "",
|
||||
snsAccessKeyId: "",
|
||||
snsSecretAccessKey: "",
|
||||
schedyBaseUrl: ENV_SCHEDY,
|
||||
schedyApiKey: ENV_SCHEDY_API_KEY,
|
||||
email: "",
|
||||
daysBefore: 10,
|
||||
};
|
||||
|
||||
export default function SettingsModal({ open, initial, onClose, onSave }: SettingsModalProps) {
|
||||
const [form, setForm] = useState<NotificationSettings>(initial || defaultSettings);
|
||||
const [testStatus, setTestStatus] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const base: any = initial || defaultSettings;
|
||||
const next: any = { ...base };
|
||||
if (next.scheduler !== 'schedy') next.scheduler = 'schedy';
|
||||
for (const k of ['cronhostApiKey', 'kronosBaseUrl', 'schedifyBaseUrl', 'schedifyApiKey', 'schedifyWebhookId']) {
|
||||
if (k in next) delete next[k];
|
||||
}
|
||||
setForm(next);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, initial]);
|
||||
|
||||
const provider: NotificationProvider | '' = form.provider || '';
|
||||
const scheduler: SchedulerProvider | '' = form.scheduler || '';
|
||||
|
||||
const saveDisabled = useMemo(() => {
|
||||
if (!form.email) return true;
|
||||
const db = form.daysBefore ?? 10;
|
||||
if (Number(db) < 0) return true;
|
||||
if (!provider) return true;
|
||||
if (!scheduler) return true;
|
||||
if (provider === 'ntfy') return !(form.ntfyServer && form.ntfyTopic);
|
||||
if (provider === 'gotify') return !(form.gotifyServer && form.gotifyToken);
|
||||
if (provider === 'sns') return !(form.snsRegion && form.snsTopicArn && form.snsAccessKeyId && form.snsSecretAccessKey);
|
||||
if (scheduler === 'schedy') return !(form.schedyBaseUrl && form.schedyApiKey);
|
||||
return true;
|
||||
}, [form, provider, scheduler]);
|
||||
|
||||
const canTest = useMemo(() => {
|
||||
// Basic same checks as save + provider must be ntfy
|
||||
if (saveDisabled) return false;
|
||||
return provider === 'ntfy' && scheduler === 'schedy';
|
||||
}, [saveDisabled, provider, scheduler]);
|
||||
|
||||
const onTest = async () => {
|
||||
setTestStatus('Scheduling test…');
|
||||
try {
|
||||
const { jobId } = await scheduleTestNotification(form);
|
||||
setTestStatus(`Test scheduled (job ${jobId}). You should receive a notification shortly.`);
|
||||
} catch (e: any) {
|
||||
setTestStatus(`Test failed: ${e?.message || String(e)}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
{scheduler === 'schedy' && (
|
||||
<>
|
||||
<label>
|
||||
<span className="text-gray-300">Schedy 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="http://localhost:8080"
|
||||
value={form.schedyBaseUrl || ''}
|
||||
onChange={(e) => setForm((f) => ({ ...f, schedyBaseUrl: e.target.value }))}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span className="text-gray-300">Schedy 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="your-secret"
|
||||
value={form.schedyApiKey || ''}
|
||||
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">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>
|
||||
<div />
|
||||
|
||||
{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 className="mt-4 flex justify-end gap-2">
|
||||
<button className="rounded bg-gray-700 px-3 py-1.5" onClick={onClose}>Cancel</button>
|
||||
<button
|
||||
className="rounded bg-emerald-600 px-3 py-1.5 disabled:opacity-50"
|
||||
disabled={!canTest}
|
||||
onClick={onTest}
|
||||
title={provider !== 'ntfy' ? 'Test currently supports ntfy provider only' : ''}
|
||||
>
|
||||
Send test alert
|
||||
</button>
|
||||
<button
|
||||
className="rounded bg-indigo-600 px-3 py-1.5 disabled:opacity-50"
|
||||
disabled={saveDisabled}
|
||||
onClick={() => onSave(form)}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
{testStatus && (
|
||||
<div className="mt-2 text-xs text-gray-300">{testStatus}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user