diff --git a/src/components/button-filter.tsx b/src/components/button-filter.tsx index 1a5a6b0..9b314fc 100644 --- a/src/components/button-filter.tsx +++ b/src/components/button-filter.tsx @@ -1,15 +1,27 @@ import classNames from "classnames"; import { ReactNode } from "react"; -export function FilterButton({ children, onClick, active }: { children: ReactNode, onClick?: () => Promise | void, active?: boolean }) { - return
- {children} +export function FilterButton({ + children, + onClick, + active, +}: { + children: ReactNode; + onClick?: () => Promise | void; + active?: boolean; +}) { + return ( +
+ {children}
-} \ No newline at end of file + ); +} diff --git a/src/components/cost.tsx b/src/components/cost.tsx index 884aeae..93d068d 100644 --- a/src/components/cost.tsx +++ b/src/components/cost.tsx @@ -6,23 +6,38 @@ interface Price { } type Cost = Price & { interval_type?: string }; -export default function CostLabel({ cost }: { cost: Cost & { other_price?: Array } }) { +export default function CostLabel({ + cost, +}: { + cost: Cost & { other_price?: Array }; +}) { const login = useLogin(); if (cost.currency === login?.currency) { - return + return ; } else { - const converted_price = cost.other_price?.find((p) => p.currency === login?.currency); + const converted_price = cost.other_price?.find( + (p) => p.currency === login?.currency, + ); if (converted_price) { - return
- - -
+ return ( +
+ + +
+ ); } else { - return + return ; } } } @@ -38,11 +53,19 @@ function intervalName(n: string) { } } -export function CostAmount({ cost, converted, className }: { cost: Cost, converted: boolean, className?: string }) { - const formatter = new Intl.NumberFormat('en-US', { - style: 'currency', +export function CostAmount({ + cost, + converted, + className, +}: { + cost: Cost; + converted: boolean; + className?: string; +}) { + const formatter = new Intl.NumberFormat("en-US", { + style: "currency", currency: cost.currency, - trailingZeroDisplay: 'stripIfInteger' + trailingZeroDisplay: "stripIfInteger", }); return (
@@ -54,4 +77,4 @@ export function CostAmount({ cost, converted, className }: { cost: Cost, convert {cost.interval_type && <>/{intervalName(cost.interval_type)}}
); -} \ No newline at end of file +} diff --git a/src/components/revolut.tsx b/src/components/revolut.tsx index 01e69d1..f70d74e 100644 --- a/src/components/revolut.tsx +++ b/src/components/revolut.tsx @@ -3,63 +3,65 @@ import { useEffect, useRef } from "react"; import { VmCostPlan } from "../api"; interface RevolutProps { - amount: VmCostPlan | { + amount: + | VmCostPlan + | { amount: number; currency: string; tax?: number; - }; - pubkey: string; - loadOrder: () => Promise; - onPaid: () => void; - onCancel?: () => void; - mode?: string; + }; + pubkey: string; + loadOrder: () => Promise; + onPaid: () => void; + onCancel?: () => void; + mode?: string; } export function RevolutPayWidget({ - pubkey, - loadOrder, - amount, - onPaid, - onCancel, - mode, + pubkey, + loadOrder, + amount, + onPaid, + onCancel, + mode, }: RevolutProps) { - const ref = useRef(null); - async function load(pubkey: string, ref: HTMLDivElement) { - const { revolutPay } = await RevolutCheckout.payments({ - locale: "auto", - mode: (mode ?? "prod") as Mode, - publicToken: pubkey, - }); - ref.innerHTML = ""; - revolutPay.mount(ref, { - currency: amount.currency, - totalAmount: amount.amount, - createOrder: async () => { - const id = await loadOrder(); - return { - publicId: id, - }; - }, - buttonStyle: { - cashback: false, - }, - }); - revolutPay.on("payment", (payload) => { - console.debug(payload); - if (payload.type === "success") { - onPaid(); - } - if (payload.type === "cancel") { - onCancel?.(); - } - }); + const ref = useRef(null); + async function load(pubkey: string, ref: HTMLDivElement) { + const { revolutPay } = await RevolutCheckout.payments({ + locale: "auto", + mode: (mode ?? "prod") as Mode, + publicToken: pubkey, + }); + ref.innerHTML = ""; + revolutPay.mount(ref, { + currency: amount.currency, + totalAmount: amount.amount, + createOrder: async () => { + const id = await loadOrder(); + return { + publicId: id, + }; + }, + buttonStyle: { + cashback: false, + }, + }); + revolutPay.on("payment", (payload) => { + console.debug(payload); + if (payload.type === "success") { + onPaid(); + } + if (payload.type === "cancel") { + onCancel?.(); + } + }); + } + + useEffect(() => { + if (ref.current) { + load(pubkey, ref.current); } + }, [pubkey, ref]); - useEffect(() => { - if (ref.current) { - load(pubkey, ref.current); - } - }, [pubkey, ref]); - - return
; + return
; } diff --git a/src/components/vps-actions.tsx b/src/components/vps-actions.tsx index f32e446..9e02aaa 100644 --- a/src/components/vps-actions.tsx +++ b/src/components/vps-actions.tsx @@ -46,19 +46,19 @@ export default function VmActions({ title="Reinstall" onClick={async (e) => { e.stopPropagation(); - if(confirm("Are you sure you want to re-install your vm?\nTHIS WILL DELETE ALL DATA!!")) { + if ( + confirm( + "Are you sure you want to re-install your vm?\nTHIS WILL DELETE ALL DATA!!", + ) + ) { await login?.api.reisntallVm(vm.id); onReload?.(); } }} className="bg-neutral-700 hover:bg-neutral-600" > - + -
); diff --git a/src/components/vps-card.tsx b/src/components/vps-card.tsx index 9c7f22e..93e68c0 100644 --- a/src/components/vps-card.tsx +++ b/src/components/vps-card.tsx @@ -8,9 +8,7 @@ export default function VpsCard({ spec }: { spec: VmTemplate }) {
{spec.name}
-
{spec.cost_plan && }
+
+ {spec.cost_plan && } +
); diff --git a/src/components/vps-custom.tsx b/src/components/vps-custom.tsx index 797fb15..0325656 100644 --- a/src/components/vps-custom.tsx +++ b/src/components/vps-custom.tsx @@ -21,8 +21,9 @@ export function VpsCustomOrder({ const [cpu, setCpu] = useState(params.min_cpu ?? 1); const [diskType, setDiskType] = useState(params.disks.at(0)); const [ram, setRam] = useState(Math.floor((params.min_memory ?? GiB) / GiB)); - const [disk, setDisk] = useState(Math.floor((diskType?.min_disk ?? GiB) / GiB)); - + const [disk, setDisk] = useState( + Math.floor((diskType?.min_disk ?? GiB) / GiB), + ); const [price, setPrice] = useState(); @@ -57,14 +58,18 @@ export function VpsCustomOrder({ return (
Custom VPS Order
- {params.disks.length > 1 &&
- {params.disks.map((d) => - setDiskType(d)}> - {d.disk_type.toUpperCase()} - - )} -
} + {params.disks.length > 1 && ( +
+ {params.disks.map((d) => ( + setDiskType(d)} + > + {d.disk_type.toUpperCase()} + + ))} +
+ )}
{cpu} CPU
-
{((payment.amount + payment.tax) / 1000).toLocaleString()} sats
- {payment.tax > 0 &&
- including {(payment.tax / 1000).toLocaleString()} sats tax -
} +
+ {((payment.amount + payment.tax) / 1000).toLocaleString()} sats +
+ {payment.tax > 0 && ( +
+ including {(payment.tax / 1000).toLocaleString()} sats tax +
+ )}
{invoice} diff --git a/src/hooks/login.tsx b/src/hooks/login.tsx index 9fee2ff..ca4a253 100644 --- a/src/hooks/login.tsx +++ b/src/hooks/login.tsx @@ -14,14 +14,15 @@ export default function useLogin() { () => session ? { - type: session.type, - publicKey: session.publicKey, - system, - currency: session.currency, - api: new LNVpsApi(ApiUrl, LoginState.getSigner()), - update: (fx: (ses: LoginSession) => void) => LoginState.updateSession(fx), - logout: () => LoginState.logout(), - } + type: session.type, + publicKey: session.publicKey, + system, + currency: session.currency, + api: new LNVpsApi(ApiUrl, LoginState.getSigner()), + update: (fx: (ses: LoginSession) => void) => + LoginState.updateSession(fx), + logout: () => LoginState.logout(), + } : undefined, [session, system], ); diff --git a/src/login.ts b/src/login.ts index c8eb294..a706fe2 100644 --- a/src/login.ts +++ b/src/login.ts @@ -43,7 +43,7 @@ class LoginStore extends ExternalStore { this.#session = { type: type ?? "nip7", publicKey: pubkey, - currency: "EUR" + currency: "EUR", }; this.#save(); } @@ -54,7 +54,7 @@ class LoginStore extends ExternalStore { type: "nsec", publicKey: s.getPubKey(), privateKey: key, - currency: "EUR" + currency: "EUR", }; this.#save(); } @@ -65,7 +65,7 @@ class LoginStore extends ExternalStore { publicKey: remotePubkey, privateKey: localKey, bunker: url, - currency: "EUR" + currency: "EUR", }; this.#save(); } diff --git a/src/pages/account-settings.tsx b/src/pages/account-settings.tsx index 8b82cfd..6f3c255 100644 --- a/src/pages/account-settings.tsx +++ b/src/pages/account-settings.tsx @@ -15,72 +15,77 @@ export function AccountSettings() { }, [login]); if (!acc) return; - return
-

- Account Settings -

+ return ( +
+

Account Settings

-
-

Country

- -
- -

Notification Settings

-
- { - setAcc((s) => - s ? { ...s, contact_email: e.target.checked } : undefined, - ); - }} - /> - Email - { - setAcc((s) => - s ? { ...s, contact_nip17: e.target.checked } : undefined, - ); - }} - /> - Nostr DM -
-
-

Email

- - setAcc((s) => (s ? { ...s, email: e.target.value } : undefined)) - } - /> - {!editEmail && ( - setEditEmail(true)} /> - )} -
-
- { - if (login?.api && acc) { - await login.api.updateAccount(acc); - const newAcc = await login.api.getAccount(); - setAcc(newAcc); - setEditEmail(false); +
+

Country

+ +
+ +

Notification Settings

+
+ { + setAcc((s) => + s ? { ...s, contact_email: e.target.checked } : undefined, + ); + }} + /> + Email + { + setAcc((s) => + s ? { ...s, contact_nip17: e.target.checked } : undefined, + ); + }} + /> + Nostr DM +
+
+

Email

+ + setAcc((s) => (s ? { ...s, email: e.target.value } : undefined)) + } + /> + {!editEmail && ( + setEditEmail(true)} /> + )} +
+
+ { + if (login?.api && acc) { + await login.api.updateAccount(acc); + const newAcc = await login.api.getAccount(); + setAcc(newAcc); + setEditEmail(false); + } + }} + > + Save + +
-
+ ); } diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 9d9862a..e954ee7 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -1,31 +1,37 @@ -import { useState, useEffect } from "react"; -import { LNVpsApi, VmHostRegion, VmTemplateResponse } from "../api"; +import { useState, useEffect, ReactNode } from "react"; +import { DiskType, LNVpsApi, VmHostRegion, VmTemplateResponse } from "../api"; import VpsCard from "../components/vps-card"; import { ApiUrl, NostrProfile } from "../const"; import { Link } from "react-router-dom"; import { VpsCustomOrder } from "../components/vps-custom"; import { LatestNews } from "../components/latest-news"; import { FilterButton } from "../components/button-filter"; -import { dedupe } from "@snort/shared"; +import { appendDedupe, dedupe } from "@snort/shared"; import useLogin from "../hooks/login"; export default function HomePage() { const login = useLogin(); const [offers, setOffers] = useState(); const [region, setRegion] = useState>([]); + const [diskType, setDiskType] = useState>([]); - const regions = (offers?.templates.map((t) => t.region) ?? []).reduce((acc, v) => { - if (acc[v.id] === undefined) { - acc[v.id] = v; - } - return acc; - }, {} as Record); + const regions = (offers?.templates.map((t) => t.region) ?? []).reduce( + (acc, v) => { + if (acc[v.id] === undefined) { + acc[v.id] = v; + } + return acc; + }, + {} as Record, + ); + const diskTypes = dedupe(offers?.templates.map((t) => t.disk_type) ?? []); useEffect(() => { const api = new LNVpsApi(ApiUrl, undefined); api.listOffers().then((o) => { - setOffers(o) + setOffers(o); setRegion(dedupe(o.templates.map((z) => z.region.id))); + setDiskType(dedupe(o.templates.map((z) => z.disk_type))); }); }, []); @@ -38,24 +44,58 @@ export default function HomePage() { Virtual Private Server hosting with flexible plans, high uptime, and dedicated support, tailored to your needs.
- {Object.keys(regions).length > 1 &&
- Regions: - {Object.values(regions).map((r) => { - return setRegion(x => { - if (x.includes(r.id)) { - return x.filter(y => y != r.id); - } else { - return [...x, r.id]; - } - })}> - {r.name} - ; - })} -
} +
+ {Object.keys(regions).length > 1 && ( + + {Object.values(regions).map((r) => { + return ( + + setRegion((x) => { + if (x.includes(r.id)) { + return x.filter((y) => y != r.id); + } else { + return appendDedupe(x, [r.id]); + } + }) + } + > + {r.name} + + ); + })} + + )} + {diskTypes.length > 1 && ( + + {diskTypes.map((d) => ( + { + setDiskType((s) => { + if (s?.includes(d)) { + return s.filter((y) => y !== d); + } else { + return appendDedupe(s, [d]); + } + }); + }} + > + {d.toUpperCase()} + + ))} + + )} +
- {offers?.templates.filter((t) => region.includes(t.region.id)).sort((a, b) => a.cost_plan.amount - b.cost_plan.amount).map((a) => )} + {offers?.templates + .filter( + (t) => + region.includes(t.region.id) && diskType.includes(t.disk_type), + ) + .sort((a, b) => a.cost_plan.amount - b.cost_plan.amount) + .map((a) => )} {offers?.templates !== undefined && offers.templates.length === 0 && (
No offers available @@ -66,7 +106,8 @@ export default function HomePage() { )} - All VPS come with 1x IPv4 and 1x IPv6 address and unmetered traffic, all prices are excluding taxes. + All VPS come with 1x IPv4 and 1x IPv6 address and unmetered traffic, + all prices are excluding taxes.
@@ -93,18 +134,29 @@ export default function HomePage() { Speedtest
- {import.meta.env.VITE_FOOTER_NOTE_1 &&
- {import.meta.env.VITE_FOOTER_NOTE_1} -
} - {import.meta.env.VITE_FOOTER_NOTE_2 &&
- {import.meta.env.VITE_FOOTER_NOTE_2} -
} + {import.meta.env.VITE_FOOTER_NOTE_1 && ( +
+ {import.meta.env.VITE_FOOTER_NOTE_1} +
+ )} + {import.meta.env.VITE_FOOTER_NOTE_2 && ( +
+ {import.meta.env.VITE_FOOTER_NOTE_2} +
+ )}
- Currency: - {" "} - + login?.update((s) => (s.currency = e.target.value)) + } + > + {["BTC", "EUR", "USD", "GBP", "AUD", "CAD", "CHF", "JPY"].map( + (a) => ( + + ), + )}
@@ -112,3 +164,18 @@ export default function HomePage() { ); } + +function FilterSection({ + header, + children, +}: { + header?: ReactNode; + children?: ReactNode; +}) { + return ( +
+
{header}
+
{children}
+
+ ); +} diff --git a/src/pages/vm-console.tsx b/src/pages/vm-console.tsx index f0daf20..deb43bb 100644 --- a/src/pages/vm-console.tsx +++ b/src/pages/vm-console.tsx @@ -10,47 +10,48 @@ import { AttachAddon } from "@xterm/addon-attach"; const fit = new FitAddon(); export function VmConsolePage() { + const { state } = useLocation() as { state?: VmInstance }; + const login = useLogin(); + const [term, setTerm] = useState(); + const termRef = useRef(null); - const { state } = useLocation() as { state?: VmInstance }; - const login = useLogin(); - const [term, setTerm] = useState(); - const termRef = useRef(null); + async function openTerminal() { + if (!login?.api || !state) return; + const ws = await login.api.connect_terminal(state.id); + const te = new Terminal(); + const webgl = new WebglAddon(); + webgl.onContextLoss(() => { + webgl.dispose(); + }); + te.loadAddon(webgl); + te.loadAddon(fit); + const attach = new AttachAddon(ws); + attach.activate(te); + setTerm((t) => { + if (t) { + t.dispose(); + } + return te; + }); + } - async function openTerminal() { - if (!login?.api || !state) return; - const ws = await login.api.connect_terminal(state.id); - const te = new Terminal(); - const webgl = new WebglAddon(); - webgl.onContextLoss(() => { - webgl.dispose(); - }); - te.loadAddon(webgl); - te.loadAddon(fit); - const attach = new AttachAddon(ws); - attach.activate(te); - setTerm((t) => { - if (t) { - t.dispose(); - } - return te - }); + useEffect(() => { + if (term && termRef.current) { + termRef.current.innerHTML = ""; + term.open(termRef.current); + term.focus(); + fit.fit(); } + }, [termRef, term]); - useEffect(() => { - if (term && termRef.current) { - termRef.current.innerHTML = ""; - term.open(termRef.current); - term.focus(); - fit.fit(); - } - }, [termRef, term]); + useEffect(() => { + openTerminal(); + }, []); - useEffect(() => { - openTerminal(); - }, []); - - return
-
VM #{state?.id} Terminal:
- {term &&
} + return ( +
+
VM #{state?.id} Terminal:
+ {term &&
}
-} \ No newline at end of file + ); +} diff --git a/src/pages/vm.tsx b/src/pages/vm.tsx index fb20a56..9a39f47 100644 --- a/src/pages/vm.tsx +++ b/src/pages/vm.tsx @@ -10,8 +10,6 @@ import { Icon } from "../components/icon"; import Modal from "../components/modal"; import SSHKeySelector from "../components/ssh-keys"; - - export default function VmPage() { const location = useLocation() as { state?: VmInstance }; const login = useLogin(); @@ -66,14 +64,9 @@ export default function VmPage() { if ((state.ip_assignments?.length ?? 0) === 0) { return
No IP's assigned
; } - return ( - <> - {state.ip_assignments?.map((i) => ipRow(i, true))} - - ); + return <>{state.ip_assignments?.map((i) => ipRow(i, true))}; } - useEffect(() => { const t = setInterval(() => reloadVmState(), 5000); return () => clearInterval(t);