diff --git a/src/api.ts b/src/api.ts index 66f234c..4812568 100644 --- a/src/api.ts +++ b/src/api.ts @@ -105,7 +105,7 @@ export class LNVpsApi { constructor( readonly url: string, readonly publisher: EventPublisher | undefined, - ) { } + ) {} async getAccount() { const { data } = await this.#handleResponse>( @@ -187,13 +187,18 @@ export class LNVpsApi { return data; } - async orderVm(template_id: number, image_id: number, ssh_key_id: number, ref_code?: string) { + async orderVm( + template_id: number, + image_id: number, + ssh_key_id: number, + ref_code?: string, + ) { const { data } = await this.#handleResponse>( await this.#req("/api/v1/vm", "POST", { template_id, image_id, ssh_key_id, - ref_code + ref_code, }), ); return data; diff --git a/src/components/button.tsx b/src/components/button.tsx index 066c413..e011527 100644 --- a/src/components/button.tsx +++ b/src/components/button.tsx @@ -22,7 +22,7 @@ const AsyncButton = forwardRef( } }} className={classNames( - "py-1 px-2 rounded-xl font-medium relative", + "py-2 px-3 rounded-xl font-medium relative", { "bg-neutral-800 cursor-not-allowed text-neutral-500": !hasBg && props.disabled === true, diff --git a/src/components/pay-button.tsx b/src/components/pay-button.tsx index 3c42419..25c3bc6 100644 --- a/src/components/pay-button.tsx +++ b/src/components/pay-button.tsx @@ -18,16 +18,18 @@ export default function VpsPayButton({ spec }: { spec: VmTemplate }) { const navigte = useNavigate(); if (!login) { - return - navigte("/login", { - state: spec, - }) - } - > - Login To Order - + return ( + + navigte("/login", { + state: spec, + }) + } + > + Login To Order + + ); } return ( {isExpired && ( <> - + e.stopPropagation()} + > Expired diff --git a/src/components/vps-payment.tsx b/src/components/vps-payment.tsx index f1b0579..6a5ea04 100644 --- a/src/components/vps-payment.tsx +++ b/src/components/vps-payment.tsx @@ -48,7 +48,9 @@ export default function VpsPayment({ className="cursor-pointer rounded-xl overflow-hidden" /> {(payment.amount / 1000).toLocaleString()} sats -
{payment.invoice}
+
+ {payment.invoice} +
); } diff --git a/src/main.tsx b/src/main.tsx index b0a9547..349f62c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -13,6 +13,7 @@ import SignUpPage from "./pages/sign-up.tsx"; import { TosPage } from "./pages/terms.tsx"; import { StatusPage } from "./pages/status.tsx"; import { AccountSettings } from "./pages/account-settings.tsx"; +import { VmBillingPage } from "./pages/vm-billing.tsx"; const system = new NostrSystem({ automaticOutboxModel: false, @@ -50,9 +51,13 @@ const router = createBrowserRouter([ element: , }, { - path: "/vm/:action?", + path: "/vm", element: , }, + { + path: "/vm/billing/:action?", + element: , + }, { path: "/tos", element: , diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 8b2ce04..4e2a76c 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -21,11 +21,12 @@ export default function HomePage() { dedicated support, tailored to your needs.
- {offers?.map((a) => ( - - ))} - {offers !== undefined && offers.length === 0 && -
No offers available
} + {offers?.map((a) => )} + {offers !== undefined && offers.length === 0 && ( +
+ No offers available +
+ )}
diff --git a/src/pages/layout.tsx b/src/pages/layout.tsx index 5155621..726b1de 100644 --- a/src/pages/layout.tsx +++ b/src/pages/layout.tsx @@ -2,7 +2,6 @@ import { Link, Outlet } from "react-router-dom"; import LoginButton from "../components/login-button"; import { saveRefCode } from "../ref"; - export default function Layout() { saveRefCode(); return ( diff --git a/src/pages/order.tsx b/src/pages/order.tsx index cf67c57..d4c87e2 100644 --- a/src/pages/order.tsx +++ b/src/pages/order.tsx @@ -31,9 +31,14 @@ export default function OrderPage() { setOrderError(""); try { const ref = getRefCode(); - const newVm = await login.api.orderVm(template.id, useImage, useSshKey, ref?.code); + const newVm = await login.api.orderVm( + template.id, + useImage, + useSshKey, + ref?.code, + ); clearRefCode(); - navigate("/vm/renew", { + navigate("/vm/billing/renew", { state: newVm, }); } catch (e) { diff --git a/src/pages/vm-billing.tsx b/src/pages/vm-billing.tsx new file mode 100644 index 0000000..95f3d6e --- /dev/null +++ b/src/pages/vm-billing.tsx @@ -0,0 +1,82 @@ +import { useCallback, useEffect, useState } from "react"; +import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; +import { VmInstance, VmPayment } from "../api"; +import VpsPayment from "../components/vps-payment"; +import useLogin from "../hooks/login"; +import { AsyncButton } from "../components/button"; +import CostLabel from "../components/cost"; + +export function VmBillingPage() { + const location = useLocation() as { state?: VmInstance }; + const params = useParams(); + const login = useLogin(); + const navigate = useNavigate(); + const [payment, setPayment] = useState(); + const [state, setState] = useState(location?.state); + + async function reloadVmState() { + if (!state) return; + const newState = await login?.api.getVm(state.id); + setState(newState); + return newState; + } + + const renew = useCallback( + async function () { + if (!login?.api || !state) return; + const p = await login?.api.renewVm(state.id); + setPayment(p); + }, + [login?.api, state], + ); + + useEffect(() => { + if (params["action"] === "renew" && login && state) { + renew() + } + }, [login, state, params, renew]); + + if (!state) return; + const expireDate = new Date(state.expires); + const days = + (expireDate.getTime() - new Date().getTime()) / 1000 / 24 / 60 / 60; + return ( +
+ + < Back + +
+
Renewal for #{state.id}
+ +
+ {days > 0 && ( +
+ Expires: {expireDate.toDateString()} ({Math.floor(days)} days) +
+ )} + {days < 0 && params["action"] !== "renew" + &&
Expired
} + {!payment && ( +
+ Extend Now +
+ )} + {payment && ( + <> +

Renew VPS

+ { + setPayment(undefined); + if (!login?.api || !state) return; + const s = await reloadVmState(); + if (params["action"] === "renew") { + navigate("/vm", { state: s }); + } + }} + /> + + )} +
+ ); +} diff --git a/src/pages/vm.tsx b/src/pages/vm.tsx index dc2a097..ddebed6 100644 --- a/src/pages/vm.tsx +++ b/src/pages/vm.tsx @@ -1,12 +1,10 @@ import "@xterm/xterm/css/xterm.css"; -import { useLocation, useNavigate, useParams } from "react-router-dom"; -import { VmInstance, VmIpAssignment, VmPayment } from "../api"; +import { useLocation, useNavigate } from "react-router-dom"; +import { VmInstance, VmIpAssignment } from "../api"; import VpsInstanceRow from "../components/vps-instance"; import useLogin from "../hooks/login"; -import { useCallback, useEffect, useRef, useState } from "react"; -import VpsPayment from "../components/vps-payment"; -import CostLabel from "../components/cost"; +import { useEffect, useRef, useState } from "react"; import { AsyncButton } from "../components/button"; import { Terminal } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"; @@ -19,11 +17,10 @@ const fit = new FitAddon(); export default function VmPage() { const location = useLocation() as { state?: VmInstance }; - const { action } = useParams(); const login = useLogin(); const navigate = useNavigate(); const [state, setState] = useState(location?.state); - const [payment, setPayment] = useState(); + const [term] = useState(); const termRef = useRef(null); const [editKey, setEditKey] = useState(false); @@ -31,15 +28,6 @@ export default function VmPage() { const [error, setError] = useState(); const [key, setKey] = useState(state?.ssh_key?.id ?? -1); - const renew = useCallback( - async function () { - if (!login?.api || !state) return; - const p = await login?.api.renewVm(state.id); - setPayment(p); - }, - [login?.api, state], - ); - async function reloadVmState() { if (!state) return; const newState = await login?.api.getVm(state.id); @@ -47,35 +35,55 @@ export default function VmPage() { } function ipRow(a: VmIpAssignment, reverse: boolean) { - return
-
- IP: - {a.ip.split("/")[0]} + return ( +
+
+ IP: + {a.ip.split("/")[0]} +
+ {a.forward_dns && ( +
+ DNS: {a.forward_dns} +
+ )} + {reverse && ( +
+
+ PTR: {a.reverse_dns} +
+ setEditReverse(a)} + /> +
+ )}
- {a.forward_dns &&
DNS: {a.forward_dns}
} - {reverse &&
-
PTR: {a.reverse_dns}
- setEditReverse(a)} /> -
} -
+ ); } function networkInfo() { if (!state) return; if ((state.ip_assignments?.length ?? 0) === 0) { - return
No IP's assigned
+ return
No IP's assigned
; } - return <> - {state.ip_assignments?.map(i => ipRow(i, true))} - {ipRow({ - id: -1, - ip: toEui64("2a13:2c0::", state.mac_address), - gateway: "" - }, false)} - + return ( + <> + {state.ip_assignments?.map((i) => ipRow(i, true))} + {ipRow( + { + id: -1, + ip: toEui64("2a13:2c0::", state.mac_address), + gateway: "", + }, + false, + )} + + ); } /*async function openTerminal() { @@ -104,17 +112,10 @@ export default function VmPage() { } }, [termRef, term, fit]); - useEffect(() => { - switch (action) { - case "renew": - renew(); - } - }, [renew, action]); - useEffect(() => { const t = setInterval(() => reloadVmState(), 5000); return () => clearInterval(t); - }, []) + }, []); if (!state) { return

No VM selected

; @@ -123,60 +124,31 @@ export default function VmPage() { return (
- {action === undefined && ( - <> -
Network:
-
- {networkInfo()} -
-
-
SSH Key:
-
- {state.ssh_key?.name} -
- setEditKey(true)} /> -
-
-
Renewal
-
-
{new Date(state.expires).toDateString()}
- {state.template?.cost_plan && ( -
- -
- )} -
- navigate("/vm/renew", { state })}> - Extend Now - - {/* + +
Network:
+
{networkInfo()}
+
+
SSH Key:
+
+ {state.ssh_key?.name} +
+ setEditKey(true)} /> +
+
+
+ navigate("/vm/billing", { state })}> + Billing + +
+ {/* {!term && Connect Terminal} {term &&
}*/} - - )} - {action === "renew" && ( - <> -

Renew VPS

- {payment && ( - { - setPayment(undefined); - if (!login?.api || !state) return; - navigate("/vm", { - state - }) - }} - /> - )} - - )} {editKey && ( setEditKey(false)}>
After selecting a new key, please restart the VM. - {error && {error}} + {error && {error}} { setError(undefined); @@ -201,15 +173,21 @@ export default function VmPage() { )} {editReverse && ( setEditReverse(undefined)}> -
Reverse DNS:
- setEditReverse({ - ...editReverse, - reverse_dns: e.target.value - })} /> + + setEditReverse({ + ...editReverse, + reverse_dns: e.target.value, + }) + } + /> DNS updates can take up to 48hrs to propagate. - {error && {error}} + {error && {error}} { setError(undefined); diff --git a/src/ref.ts b/src/ref.ts index d86bbd3..472e014 100644 --- a/src/ref.ts +++ b/src/ref.ts @@ -1,34 +1,37 @@ export interface RefCode { - code: string; - saved: number; + code: string; + saved: number; } export function saveRefCode() { - const search = new URLSearchParams(window.location.search); - const code = search.get("ref"); - if (code) { - // save or overwrite new code from landing - window.localStorage.setItem("ref", JSON.stringify({ - code, - saved: Math.floor(new Date().getTime() / 1000) - })); - window.location.search = ""; - } + const search = new URLSearchParams(window.location.search); + const code = search.get("ref"); + if (code) { + // save or overwrite new code from landing + window.localStorage.setItem( + "ref", + JSON.stringify({ + code, + saved: Math.floor(new Date().getTime() / 1000), + }), + ); + window.location.search = ""; + } } export function getRefCode() { - const ref = window.localStorage.getItem("ref"); - if (ref) { - const refObj = JSON.parse(ref) as RefCode; - const now = Math.floor(new Date().getTime() / 1000); - // treat code as stale if > 7days old - if (Math.abs(refObj.saved - now) > 604800) { - window.localStorage.removeItem("ref"); - } - return refObj; + const ref = window.localStorage.getItem("ref"); + if (ref) { + const refObj = JSON.parse(ref) as RefCode; + const now = Math.floor(new Date().getTime() / 1000); + // treat code as stale if > 7days old + if (Math.abs(refObj.saved - now) > 604800) { + window.localStorage.removeItem("ref"); } + return refObj; + } } export function clearRefCode() { - window.localStorage.removeItem("ref"); -} \ No newline at end of file + window.localStorage.removeItem("ref"); +}