feat: notification settings
All checks were successful
continuous-integration/drone Build is passing

This commit is contained in:
2025-02-21 11:43:16 +00:00
parent 31f0a3c925
commit 8758116520
4 changed files with 98 additions and 28 deletions

View File

@ -9,6 +9,12 @@ export type ApiResponse<T> = ApiResponseBase & {
data: T;
};
export interface AccountDetail {
email?: string;
contact_nip17: boolean;
contact_email: boolean;
}
export interface VmCostPlan {
id: number;
name: string;
@ -108,7 +114,21 @@ export class LNVpsApi {
constructor(
readonly url: string,
readonly publisher: EventPublisher | undefined,
) {}
) { }
async getAccount() {
const { data } = await this.#handleResponse<ApiResponse<AccountDetail>>(
await this.#req("/api/v1/account", "GET"),
);
return data;
}
async updateAccount(acc: AccountDetail) {
const { data } = await this.#handleResponse<ApiResponse<void>>(
await this.#req("/api/v1/account", "PATCH", acc),
);
return data;
}
async listVms() {
const { data } = await this.#handleResponse<ApiResponse<Array<VmInstance>>>(

View File

@ -1,4 +1,4 @@
import { useContext, useSyncExternalStore } from "react";
import { useContext, useMemo, useSyncExternalStore } from "react";
import { LoginState } from "../login";
import { SnortContext } from "@snort/system-react";
import { LNVpsApi } from "../api";
@ -10,12 +10,12 @@ export default function useLogin() {
() => LoginState.snapshot(),
);
const system = useContext(SnortContext);
return session
return useMemo(() => session
? {
type: session.type,
publicKey: session.publicKey,
system,
api: new LNVpsApi(ApiUrl, LoginState.getSigner()),
}
: undefined;
: undefined, [session, system]);
}

View File

@ -41,3 +41,7 @@ textarea,
select {
@apply border-none rounded-xl bg-neutral-900 p-2;
}
input:disabled {
@apply text-neutral-200/50;
}

View File

@ -1,45 +1,91 @@
import { useEffect, useState } from "react";
import { VmInstance } from "../api";
import { AccountDetail, LNVpsApi, VmInstance } from "../api";
import useLogin from "../hooks/login";
import VpsInstanceRow from "../components/vps-instance";
import { hexToBech32 } from "@snort/shared";
import { Icon } from "../components/icon";
import { AsyncButton } from "../components/button";
export default function AccountPage() {
const login = useLogin();
const [acc, setAcc] = useState<AccountDetail>();
const [editEmail, setEditEmail] = useState(false);
const [vms, setVms] = useState<Array<VmInstance>>([]);
async function loadVms() {
if (!login?.api) return;
const vms = await login?.api.listVms();
async function loadVms(api: LNVpsApi) {
const vms = await api.listVms();
setVms(vms);
}
useEffect(() => {
loadVms();
const t = setInterval(() => loadVms(), 5_000);
return () => clearInterval(t);
if (login?.api) {
loadVms(login.api);
login.api.getAccount().then(setAcc);
const t = setInterval(() => {
loadVms(login.api);
}, 5_000);
return () => clearInterval(t);
}
}, [login]);
const npub = hexToBech32("npub", login?.publicKey);
return (
<>
<div className="flex flex-col gap-2">
Your Public Key:
<pre className="bg-neutral-800 rounded-md px-3 py-2 select-all text-sm">{npub}</pre>
function notifications() {
return <>
<h3>Notification Settings</h3>
<div className="flex gap-2 items-center">
<input type="checkbox" checked={acc?.contact_email ?? false} onChange={(e) => {
setAcc((s) => (s ? { ...s, contact_email: e.target.checked } : undefined));
}} />
Email
<input type="checkbox" checked={acc?.contact_nip17 ?? false} onChange={(e) => {
setAcc((s) => (s ? { ...s, contact_nip17: e.target.checked } : undefined));
}} />
Nostr DM
</div>
<h3>My Resources</h3>
<div className="rounded-xl bg-red-400 text-black p-3 font-bold">
Something doesnt look right? <br />
Please contact support on: {" "}
<a href={`mailto:sales@lnvps.net?subject=[${npub}]%20Account%20Query`} className="underline">
sales@lnvps.net
</a>
<div className="flex gap-2 items-center">
<h4>Email</h4>
<input type="text" disabled={!editEmail} value={acc?.email} onChange={e => setAcc(s => (s ? { ...s, email: e.target.value } : undefined))} />
{!editEmail && <Icon name="pencil" onClick={() => setEditEmail(true)} />}
</div>
<div className="flex flex-col gap-2">
{vms.map((a) => (
<VpsInstanceRow key={a.id} vm={a} onReload={loadVms} />
))}
<div>
<AsyncButton onClick={async () => {
if (login?.api && acc) {
await login.api.updateAccount(acc);
const newAcc = await login.api.getAccount();
setAcc(newAcc);
setEditEmail(false);
}
}}>
Save
</AsyncButton>
</div>
</>
}
const npub = hexToBech32("npub", login?.publicKey);
const subjectLine = `[${npub}] Account Query`;
return (
<div className="flex flex-col gap-2">
Your Public Key:
<pre className="bg-neutral-900 rounded-md px-3 py-2 select-all text-sm">{npub}</pre>
{notifications()}
<h3>My Resources</h3>
<div className="rounded-xl bg-red-400 text-black p-3">
Something doesnt look right? <br />
Please contact support on: {" "}
<a href={`mailto:sales@lnvps.net?subject=${encodeURIComponent(subjectLine)}`} className="underline">
sales@lnvps.net
</a>
<br />
<b>Please include your public key in all communications.</b>
</div>
{vms.map((a) => (
<VpsInstanceRow key={a.id} vm={a} onReload={() => {
if (login?.api) {
loadVms(login.api);
}
}} />
))}
</div>
);
}