feat: new billing page
This commit is contained in:
11
src/api.ts
11
src/api.ts
@ -105,7 +105,7 @@ export class LNVpsApi {
|
|||||||
constructor(
|
constructor(
|
||||||
readonly url: string,
|
readonly url: string,
|
||||||
readonly publisher: EventPublisher | undefined,
|
readonly publisher: EventPublisher | undefined,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
async getAccount() {
|
async getAccount() {
|
||||||
const { data } = await this.#handleResponse<ApiResponse<AccountDetail>>(
|
const { data } = await this.#handleResponse<ApiResponse<AccountDetail>>(
|
||||||
@ -187,13 +187,18 @@ export class LNVpsApi {
|
|||||||
return data;
|
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<ApiResponse<VmInstance>>(
|
const { data } = await this.#handleResponse<ApiResponse<VmInstance>>(
|
||||||
await this.#req("/api/v1/vm", "POST", {
|
await this.#req("/api/v1/vm", "POST", {
|
||||||
template_id,
|
template_id,
|
||||||
image_id,
|
image_id,
|
||||||
ssh_key_id,
|
ssh_key_id,
|
||||||
ref_code
|
ref_code,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
|
@ -22,7 +22,7 @@ const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>(
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={classNames(
|
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":
|
"bg-neutral-800 cursor-not-allowed text-neutral-500":
|
||||||
!hasBg && props.disabled === true,
|
!hasBg && props.disabled === true,
|
||||||
|
@ -18,16 +18,18 @@ export default function VpsPayButton({ spec }: { spec: VmTemplate }) {
|
|||||||
const navigte = useNavigate();
|
const navigte = useNavigate();
|
||||||
|
|
||||||
if (!login) {
|
if (!login) {
|
||||||
return <AsyncButton
|
return (
|
||||||
className={`${classNames} bg-red-900`}
|
<AsyncButton
|
||||||
onClick={() =>
|
className={`${classNames} bg-red-900`}
|
||||||
navigte("/login", {
|
onClick={() =>
|
||||||
state: spec,
|
navigte("/login", {
|
||||||
})
|
state: spec,
|
||||||
}
|
})
|
||||||
>
|
}
|
||||||
Login To Order
|
>
|
||||||
</AsyncButton>
|
Login To Order
|
||||||
|
</AsyncButton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
|
@ -41,7 +41,12 @@ export default function VpsInstanceRow({
|
|||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
{isExpired && (
|
{isExpired && (
|
||||||
<>
|
<>
|
||||||
<Link to="/vm/renew" className="text-red-500 text-sm" state={vm}>
|
<Link
|
||||||
|
to="/vm/billing/renew"
|
||||||
|
className="text-red-500 text-sm"
|
||||||
|
state={vm}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
Expired
|
Expired
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
|
@ -48,7 +48,9 @@ export default function VpsPayment({
|
|||||||
className="cursor-pointer rounded-xl overflow-hidden"
|
className="cursor-pointer rounded-xl overflow-hidden"
|
||||||
/>
|
/>
|
||||||
{(payment.amount / 1000).toLocaleString()} sats
|
{(payment.amount / 1000).toLocaleString()} sats
|
||||||
<div className="monospace select-all break-all text-center text-sm">{payment.invoice}</div>
|
<div className="monospace select-all break-all text-center text-sm">
|
||||||
|
{payment.invoice}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import SignUpPage from "./pages/sign-up.tsx";
|
|||||||
import { TosPage } from "./pages/terms.tsx";
|
import { TosPage } from "./pages/terms.tsx";
|
||||||
import { StatusPage } from "./pages/status.tsx";
|
import { StatusPage } from "./pages/status.tsx";
|
||||||
import { AccountSettings } from "./pages/account-settings.tsx";
|
import { AccountSettings } from "./pages/account-settings.tsx";
|
||||||
|
import { VmBillingPage } from "./pages/vm-billing.tsx";
|
||||||
|
|
||||||
const system = new NostrSystem({
|
const system = new NostrSystem({
|
||||||
automaticOutboxModel: false,
|
automaticOutboxModel: false,
|
||||||
@ -50,9 +51,13 @@ const router = createBrowserRouter([
|
|||||||
element: <OrderPage />,
|
element: <OrderPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/vm/:action?",
|
path: "/vm",
|
||||||
element: <VmPage />,
|
element: <VmPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/vm/billing/:action?",
|
||||||
|
element: <VmBillingPage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/tos",
|
path: "/tos",
|
||||||
element: <TosPage />,
|
element: <TosPage />,
|
||||||
|
@ -21,11 +21,12 @@ export default function HomePage() {
|
|||||||
dedicated support, tailored to your needs.
|
dedicated support, tailored to your needs.
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
{offers?.map((a) => (
|
{offers?.map((a) => <VpsCard spec={a} key={a.id} />)}
|
||||||
<VpsCard spec={a} key={a.id} />
|
{offers !== undefined && offers.length === 0 && (
|
||||||
))}
|
<div className="text-red-500 bold text-xl uppercase">
|
||||||
{offers !== undefined && offers.length === 0 &&
|
No offers available
|
||||||
<div className="text-red-500 bold text-xl uppercase">No offers available</div>}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small className="text-neutral-400">
|
<small className="text-neutral-400">
|
||||||
|
@ -2,7 +2,6 @@ import { Link, Outlet } from "react-router-dom";
|
|||||||
import LoginButton from "../components/login-button";
|
import LoginButton from "../components/login-button";
|
||||||
import { saveRefCode } from "../ref";
|
import { saveRefCode } from "../ref";
|
||||||
|
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
saveRefCode();
|
saveRefCode();
|
||||||
return (
|
return (
|
||||||
|
@ -31,9 +31,14 @@ export default function OrderPage() {
|
|||||||
setOrderError("");
|
setOrderError("");
|
||||||
try {
|
try {
|
||||||
const ref = getRefCode();
|
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();
|
clearRefCode();
|
||||||
navigate("/vm/renew", {
|
navigate("/vm/billing/renew", {
|
||||||
state: newVm,
|
state: newVm,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
82
src/pages/vm-billing.tsx
Normal file
82
src/pages/vm-billing.tsx
Normal file
@ -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<VmPayment>();
|
||||||
|
const [state, setState] = useState<VmInstance | undefined>(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 (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Link to={"/vm"} state={state}>
|
||||||
|
< Back
|
||||||
|
</Link>
|
||||||
|
<div className="text-xl bg-neutral-900 rounded-xl px-3 py-4 flex justify-between items-center">
|
||||||
|
<div>Renewal for #{state.id}</div>
|
||||||
|
<CostLabel cost={state.template.cost_plan} />
|
||||||
|
</div>
|
||||||
|
{days > 0 && (
|
||||||
|
<div>
|
||||||
|
Expires: {expireDate.toDateString()} ({Math.floor(days)} days)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{days < 0 && params["action"] !== "renew"
|
||||||
|
&& <div className="text-red-500 text-xl">Expired</div>}
|
||||||
|
{!payment && (
|
||||||
|
<div>
|
||||||
|
<AsyncButton onClick={renew}>Extend Now</AsyncButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{payment && (
|
||||||
|
<>
|
||||||
|
<h3>Renew VPS</h3>
|
||||||
|
<VpsPayment
|
||||||
|
payment={payment}
|
||||||
|
onPaid={async () => {
|
||||||
|
setPayment(undefined);
|
||||||
|
if (!login?.api || !state) return;
|
||||||
|
const s = await reloadVmState();
|
||||||
|
if (params["action"] === "renew") {
|
||||||
|
navigate("/vm", { state: s });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
176
src/pages/vm.tsx
176
src/pages/vm.tsx
@ -1,12 +1,10 @@
|
|||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
|
|
||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { VmInstance, VmIpAssignment, VmPayment } from "../api";
|
import { VmInstance, VmIpAssignment } from "../api";
|
||||||
import VpsInstanceRow from "../components/vps-instance";
|
import VpsInstanceRow from "../components/vps-instance";
|
||||||
import useLogin from "../hooks/login";
|
import useLogin from "../hooks/login";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import VpsPayment from "../components/vps-payment";
|
|
||||||
import CostLabel from "../components/cost";
|
|
||||||
import { AsyncButton } from "../components/button";
|
import { AsyncButton } from "../components/button";
|
||||||
import { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
import { FitAddon } from "@xterm/addon-fit";
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
@ -19,11 +17,10 @@ const fit = new FitAddon();
|
|||||||
|
|
||||||
export default function VmPage() {
|
export default function VmPage() {
|
||||||
const location = useLocation() as { state?: VmInstance };
|
const location = useLocation() as { state?: VmInstance };
|
||||||
const { action } = useParams();
|
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [state, setState] = useState<VmInstance | undefined>(location?.state);
|
const [state, setState] = useState<VmInstance | undefined>(location?.state);
|
||||||
const [payment, setPayment] = useState<VmPayment>();
|
|
||||||
const [term] = useState<Terminal>();
|
const [term] = useState<Terminal>();
|
||||||
const termRef = useRef<HTMLDivElement | null>(null);
|
const termRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [editKey, setEditKey] = useState(false);
|
const [editKey, setEditKey] = useState(false);
|
||||||
@ -31,15 +28,6 @@ export default function VmPage() {
|
|||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const [key, setKey] = useState(state?.ssh_key?.id ?? -1);
|
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() {
|
async function reloadVmState() {
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
const newState = await login?.api.getVm(state.id);
|
const newState = await login?.api.getVm(state.id);
|
||||||
@ -47,35 +35,55 @@ export default function VmPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ipRow(a: VmIpAssignment, reverse: boolean) {
|
function ipRow(a: VmIpAssignment, reverse: boolean) {
|
||||||
return <div
|
return (
|
||||||
key={a.id}
|
<div
|
||||||
className="bg-neutral-900 px-2 py-3 rounded-lg flex gap-2 flex-col justify-center"
|
key={a.id}
|
||||||
>
|
className="bg-neutral-900 px-2 py-3 rounded-lg flex gap-2 flex-col justify-center"
|
||||||
<div>
|
>
|
||||||
<span className="select-none">IP: </span>
|
<div>
|
||||||
<span className="select-all">{a.ip.split("/")[0]}</span>
|
<span className="select-none">IP: </span>
|
||||||
|
<span className="select-all">{a.ip.split("/")[0]}</span>
|
||||||
|
</div>
|
||||||
|
{a.forward_dns && (
|
||||||
|
<div className="text-sm select-none">
|
||||||
|
DNS: <span className="select-all">{a.forward_dns}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{reverse && (
|
||||||
|
<div className="text-sm select-none flex items-center gap-2">
|
||||||
|
<div>
|
||||||
|
PTR: <span className="select-all">{a.reverse_dns}</span>
|
||||||
|
</div>
|
||||||
|
<Icon
|
||||||
|
name="pencil"
|
||||||
|
className="inline"
|
||||||
|
size={15}
|
||||||
|
onClick={() => setEditReverse(a)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{a.forward_dns && <div className="text-sm select-none">DNS: <span className="select-all">{a.forward_dns}</span></div>}
|
);
|
||||||
{reverse && <div className="text-sm select-none flex items-center gap-2">
|
|
||||||
<div>PTR: <span className="select-all">{a.reverse_dns}</span></div>
|
|
||||||
<Icon name="pencil" className="inline" size={15} onClick={() => setEditReverse(a)} />
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function networkInfo() {
|
function networkInfo() {
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
if ((state.ip_assignments?.length ?? 0) === 0) {
|
if ((state.ip_assignments?.length ?? 0) === 0) {
|
||||||
return <div className="text-sm text-red-500">No IP's assigned</div>
|
return <div className="text-sm text-red-500">No IP's assigned</div>;
|
||||||
}
|
}
|
||||||
return <>
|
return (
|
||||||
{state.ip_assignments?.map(i => ipRow(i, true))}
|
<>
|
||||||
{ipRow({
|
{state.ip_assignments?.map((i) => ipRow(i, true))}
|
||||||
id: -1,
|
{ipRow(
|
||||||
ip: toEui64("2a13:2c0::", state.mac_address),
|
{
|
||||||
gateway: ""
|
id: -1,
|
||||||
}, false)}
|
ip: toEui64("2a13:2c0::", state.mac_address),
|
||||||
</>
|
gateway: "",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*async function openTerminal() {
|
/*async function openTerminal() {
|
||||||
@ -104,17 +112,10 @@ export default function VmPage() {
|
|||||||
}
|
}
|
||||||
}, [termRef, term, fit]);
|
}, [termRef, term, fit]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
switch (action) {
|
|
||||||
case "renew":
|
|
||||||
renew();
|
|
||||||
}
|
|
||||||
}, [renew, action]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const t = setInterval(() => reloadVmState(), 5000);
|
const t = setInterval(() => reloadVmState(), 5000);
|
||||||
return () => clearInterval(t);
|
return () => clearInterval(t);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return <h2>No VM selected</h2>;
|
return <h2>No VM selected</h2>;
|
||||||
@ -123,60 +124,31 @@ export default function VmPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<VpsInstanceRow vm={state} actions={true} />
|
<VpsInstanceRow vm={state} actions={true} />
|
||||||
{action === undefined && (
|
|
||||||
<>
|
<div className="text-xl">Network:</div>
|
||||||
<div className="text-xl">Network:</div>
|
<div className="grid grid-cols-2 gap-4">{networkInfo()}</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="flex gap-2 items-center">
|
||||||
{networkInfo()}
|
<div className="text-xl">SSH Key:</div>
|
||||||
</div>
|
<div className="text-sm bg-neutral-900 px-3 py-1 rounded-lg">
|
||||||
<div className="flex gap-2 items-center">
|
{state.ssh_key?.name}
|
||||||
<div className="text-xl">SSH Key:</div>
|
</div>
|
||||||
<div className="text-sm bg-neutral-900 px-3 py-1 rounded-lg">
|
<Icon name="pencil" onClick={() => setEditKey(true)} />
|
||||||
{state.ssh_key?.name}
|
</div>
|
||||||
</div>
|
<hr />
|
||||||
<Icon name="pencil" onClick={() => setEditKey(true)} />
|
<div className="flex gap-4">
|
||||||
</div>
|
<AsyncButton onClick={() => navigate("/vm/billing", { state })}>
|
||||||
<hr />
|
Billing
|
||||||
<div className="text-xl">Renewal</div>
|
</AsyncButton>
|
||||||
<div className="flex justify-between items-center">
|
</div>
|
||||||
<div>{new Date(state.expires).toDateString()}</div>
|
{/*
|
||||||
{state.template?.cost_plan && (
|
|
||||||
<div>
|
|
||||||
<CostLabel cost={state.template?.cost_plan} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<AsyncButton onClick={() => navigate("/vm/renew", { state })}>
|
|
||||||
Extend Now
|
|
||||||
</AsyncButton>
|
|
||||||
{/*
|
|
||||||
{!term && <AsyncButton onClick={openTerminal}>Connect Terminal</AsyncButton>}
|
{!term && <AsyncButton onClick={openTerminal}>Connect Terminal</AsyncButton>}
|
||||||
{term && <div className="border p-2" ref={termRef}></div>}*/}
|
{term && <div className="border p-2" ref={termRef}></div>}*/}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{action === "renew" && (
|
|
||||||
<>
|
|
||||||
<h3>Renew VPS</h3>
|
|
||||||
{payment && (
|
|
||||||
<VpsPayment
|
|
||||||
payment={payment}
|
|
||||||
onPaid={async () => {
|
|
||||||
setPayment(undefined);
|
|
||||||
if (!login?.api || !state) return;
|
|
||||||
navigate("/vm", {
|
|
||||||
state
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{editKey && (
|
{editKey && (
|
||||||
<Modal id="edit-ssh-key" onClose={() => setEditKey(false)}>
|
<Modal id="edit-ssh-key" onClose={() => setEditKey(false)}>
|
||||||
<SSHKeySelector selectedKey={key} setSelectedKey={setKey} />
|
<SSHKeySelector selectedKey={key} setSelectedKey={setKey} />
|
||||||
<div className="flex flex-col gap-4 mt-8">
|
<div className="flex flex-col gap-4 mt-8">
|
||||||
<small>After selecting a new key, please restart the VM.</small>
|
<small>After selecting a new key, please restart the VM.</small>
|
||||||
{error && <b className="text-red-700">{error}</b>}
|
{error && <b className="text-red-500">{error}</b>}
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
@ -201,15 +173,21 @@ export default function VmPage() {
|
|||||||
)}
|
)}
|
||||||
{editReverse && (
|
{editReverse && (
|
||||||
<Modal id="edit-reverse" onClose={() => setEditReverse(undefined)}>
|
<Modal id="edit-reverse" onClose={() => setEditReverse(undefined)}>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="text-lg">Reverse DNS:</div>
|
<div className="text-lg">Reverse DNS:</div>
|
||||||
<input type="text" placeholder="my-domain.com" value={editReverse.reverse_dns} onChange={(e) => setEditReverse({
|
<input
|
||||||
...editReverse,
|
type="text"
|
||||||
reverse_dns: e.target.value
|
placeholder="my-domain.com"
|
||||||
})} />
|
value={editReverse.reverse_dns}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEditReverse({
|
||||||
|
...editReverse,
|
||||||
|
reverse_dns: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
<small>DNS updates can take up to 48hrs to propagate.</small>
|
<small>DNS updates can take up to 48hrs to propagate.</small>
|
||||||
{error && <b className="text-red-700">{error}</b>}
|
{error && <b className="text-red-500">{error}</b>}
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
|
49
src/ref.ts
49
src/ref.ts
@ -1,34 +1,37 @@
|
|||||||
export interface RefCode {
|
export interface RefCode {
|
||||||
code: string;
|
code: string;
|
||||||
saved: number;
|
saved: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveRefCode() {
|
export function saveRefCode() {
|
||||||
const search = new URLSearchParams(window.location.search);
|
const search = new URLSearchParams(window.location.search);
|
||||||
const code = search.get("ref");
|
const code = search.get("ref");
|
||||||
if (code) {
|
if (code) {
|
||||||
// save or overwrite new code from landing
|
// save or overwrite new code from landing
|
||||||
window.localStorage.setItem("ref", JSON.stringify({
|
window.localStorage.setItem(
|
||||||
code,
|
"ref",
|
||||||
saved: Math.floor(new Date().getTime() / 1000)
|
JSON.stringify({
|
||||||
}));
|
code,
|
||||||
window.location.search = "";
|
saved: Math.floor(new Date().getTime() / 1000),
|
||||||
}
|
}),
|
||||||
|
);
|
||||||
|
window.location.search = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRefCode() {
|
export function getRefCode() {
|
||||||
const ref = window.localStorage.getItem("ref");
|
const ref = window.localStorage.getItem("ref");
|
||||||
if (ref) {
|
if (ref) {
|
||||||
const refObj = JSON.parse(ref) as RefCode;
|
const refObj = JSON.parse(ref) as RefCode;
|
||||||
const now = Math.floor(new Date().getTime() / 1000);
|
const now = Math.floor(new Date().getTime() / 1000);
|
||||||
// treat code as stale if > 7days old
|
// treat code as stale if > 7days old
|
||||||
if (Math.abs(refObj.saved - now) > 604800) {
|
if (Math.abs(refObj.saved - now) > 604800) {
|
||||||
window.localStorage.removeItem("ref");
|
window.localStorage.removeItem("ref");
|
||||||
}
|
|
||||||
return refObj;
|
|
||||||
}
|
}
|
||||||
|
return refObj;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearRefCode() {
|
export function clearRefCode() {
|
||||||
window.localStorage.removeItem("ref");
|
window.localStorage.removeItem("ref");
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user