This commit is contained in:
85
src/api.ts
85
src/api.ts
@ -9,6 +9,17 @@ export type ApiResponse<T> = ApiResponseBase & {
|
||||
data: T;
|
||||
};
|
||||
|
||||
export enum DiskType {
|
||||
SSD = "ssd",
|
||||
HDD = "hdd",
|
||||
}
|
||||
|
||||
export enum DiskInterface {
|
||||
SATA = "sata",
|
||||
SCSI = "scsi",
|
||||
PCIe = "pcid",
|
||||
}
|
||||
|
||||
export interface AccountDetail {
|
||||
email?: string;
|
||||
contact_nip17: boolean;
|
||||
@ -19,7 +30,7 @@ export interface VmCostPlan {
|
||||
id: number;
|
||||
name: string;
|
||||
amount: number;
|
||||
currency: "EUR" | "BTC";
|
||||
currency: string;
|
||||
interval_amount: number;
|
||||
interval_type: string;
|
||||
}
|
||||
@ -29,16 +40,54 @@ export interface VmHostRegion {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface VmCustomTemplateParams {
|
||||
id: number;
|
||||
name: string;
|
||||
region: VmHostRegion;
|
||||
max_cpu: number;
|
||||
min_cpu: number;
|
||||
min_memory: number;
|
||||
max_memory: number;
|
||||
min_disk: number;
|
||||
max_disk: number;
|
||||
disks: Array<VmCustomTemplateDiskParams>;
|
||||
}
|
||||
|
||||
export interface VmCustomTemplateDiskParams {
|
||||
disk_type: DiskType;
|
||||
disk_interface: DiskInterface;
|
||||
}
|
||||
|
||||
export interface VmCustomTemplateRequest {
|
||||
pricing_id: number;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
disk: number;
|
||||
disk_type: DiskType;
|
||||
disk_interface: DiskInterface;
|
||||
}
|
||||
|
||||
export interface VmCustomPrice {
|
||||
currency: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface VmTemplateResponse {
|
||||
templates: Array<VmTemplate>;
|
||||
custom_template?: Array<VmCustomTemplateParams>;
|
||||
}
|
||||
|
||||
export interface VmTemplate {
|
||||
id: number;
|
||||
pricing_id?: number;
|
||||
name: string;
|
||||
created: Date;
|
||||
expires?: Date;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
disk_size: number;
|
||||
disk_type: string;
|
||||
disk_interface: string;
|
||||
disk_type: DiskType;
|
||||
disk_interface: DiskInterface;
|
||||
cost_plan: VmCostPlan;
|
||||
region: VmHostRegion;
|
||||
}
|
||||
@ -175,9 +224,9 @@ export class LNVpsApi {
|
||||
}
|
||||
|
||||
async listOffers() {
|
||||
const { data } = await this.#handleResponse<ApiResponse<Array<VmTemplate>>>(
|
||||
await this.#req("/api/v1/vm/templates", "GET"),
|
||||
);
|
||||
const { data } = await this.#handleResponse<
|
||||
ApiResponse<VmTemplateResponse>
|
||||
>(await this.#req("/api/v1/vm/templates", "GET"));
|
||||
return data;
|
||||
}
|
||||
|
||||
@ -222,6 +271,30 @@ export class LNVpsApi {
|
||||
return data;
|
||||
}
|
||||
|
||||
async customPrice(req: VmCustomTemplateRequest) {
|
||||
const { data } = await this.#handleResponse<ApiResponse<VmCustomPrice>>(
|
||||
await this.#req("/api/v1/vm/custom-template/price", "POST", req),
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async orderCustom(
|
||||
req: VmCustomTemplateRequest,
|
||||
image_id: number,
|
||||
ssh_key_id: number,
|
||||
ref_code?: string,
|
||||
) {
|
||||
const { data } = await this.#handleResponse<ApiResponse<VmInstance>>(
|
||||
await this.#req("/api/v1/vm/custom-template", "POST", {
|
||||
...req,
|
||||
image_id,
|
||||
ssh_key_id,
|
||||
ref_code,
|
||||
}),
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async renewVm(vm_id: number) {
|
||||
const { data } = await this.#handleResponse<ApiResponse<VmPayment>>(
|
||||
await this.#req(`/api/v1/vm/${vm_id}/renew`, "GET"),
|
||||
|
@ -14,7 +14,8 @@ export default function CostLabel({ cost }: { cost: VmCostPlan }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{cost.amount} {cost.currency}/{intervalName(cost.interval_type)}
|
||||
{cost.amount.toFixed(2)} {cost.currency}/
|
||||
{intervalName(cost.interval_type)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -21,11 +21,7 @@ export default function VpsPayButton({ spec }: { spec: VmTemplate }) {
|
||||
return (
|
||||
<AsyncButton
|
||||
className={`${classNames} bg-red-900`}
|
||||
onClick={() =>
|
||||
navigte("/login", {
|
||||
state: spec,
|
||||
})
|
||||
}
|
||||
onClick={() => navigte("/login")}
|
||||
>
|
||||
Login To Order
|
||||
</AsyncButton>
|
||||
|
122
src/components/vps-custom.tsx
Normal file
122
src/components/vps-custom.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
DiskInterface,
|
||||
DiskType,
|
||||
LNVpsApi,
|
||||
VmCustomPrice,
|
||||
VmCustomTemplateParams,
|
||||
} from "../api";
|
||||
import { ApiUrl, GiB } from "../const";
|
||||
import CostLabel from "./cost";
|
||||
import VpsPayButton from "./pay-button";
|
||||
|
||||
export function VpsCustomOrder({
|
||||
templates,
|
||||
}: {
|
||||
templates: Array<VmCustomTemplateParams>;
|
||||
}) {
|
||||
const [region] = useState(templates.at(0)?.region.id);
|
||||
const params = templates.find((t) => t.region.id == region) ?? templates[0];
|
||||
const [cpu, setCpu] = useState(params.min_cpu ?? 1);
|
||||
const [ram, setRam] = useState(Math.floor((params.min_memory ?? GiB) / GiB));
|
||||
const [disk, setDisk] = useState(Math.floor((params.min_disk ?? GiB) / GiB));
|
||||
const [diskType] = useState(params.disks.at(0));
|
||||
|
||||
const [price, setPrice] = useState<VmCustomPrice>();
|
||||
|
||||
const cost_plan = {
|
||||
id: 0,
|
||||
name: "custom",
|
||||
amount: price?.amount ?? 0,
|
||||
currency: price?.currency ?? "",
|
||||
interval_amount: 1,
|
||||
interval_type: "month",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
const api = new LNVpsApi(ApiUrl, undefined);
|
||||
api
|
||||
.customPrice({
|
||||
pricing_id: params.id,
|
||||
cpu,
|
||||
memory: ram * GiB,
|
||||
disk: disk * GiB,
|
||||
disk_type: diskType?.disk_type ?? DiskType.SSD,
|
||||
disk_interface: diskType?.disk_interface ?? DiskInterface.PCIe,
|
||||
})
|
||||
.then(setPrice);
|
||||
}, 500);
|
||||
return () => clearTimeout(t);
|
||||
}, [region, cpu, ram, disk, diskType, params]);
|
||||
|
||||
if (templates.length == 0) return;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 bg-neutral-900 rounded-xl px-4 py-6">
|
||||
<div className="text-lg">Custom VPS Order</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="min-w-[100px]">{cpu} CPU</div>
|
||||
<input
|
||||
type="range"
|
||||
value={cpu}
|
||||
onChange={(e) => setCpu(e.target.valueAsNumber)}
|
||||
min={params.min_cpu}
|
||||
max={params.max_cpu}
|
||||
step={1}
|
||||
className="grow"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="min-w-[100px]">{ram.toString()} GB RAM</div>
|
||||
<input
|
||||
type="range"
|
||||
value={ram}
|
||||
onChange={(e) => setRam(e.target.valueAsNumber)}
|
||||
min={Math.floor(params.min_memory / GiB)}
|
||||
max={Math.floor(params.max_memory / GiB)}
|
||||
step={1}
|
||||
className="grow"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="min-w-[100px]">
|
||||
{disk.toString()} GB {diskType?.disk_type.toLocaleUpperCase()}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
value={disk}
|
||||
onChange={(e) => setDisk(e.target.valueAsNumber)}
|
||||
min={Math.floor(params.min_disk / GiB)}
|
||||
max={Math.floor(params.max_disk / GiB)}
|
||||
step={1}
|
||||
className="grow"
|
||||
/>
|
||||
</div>
|
||||
{price && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xl flex-1">
|
||||
<CostLabel cost={cost_plan} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<VpsPayButton
|
||||
spec={{
|
||||
id: 0,
|
||||
pricing_id: params.id,
|
||||
cpu,
|
||||
name: "Custom",
|
||||
memory: ram * GiB,
|
||||
disk_size: disk * GiB,
|
||||
disk_type: diskType?.disk_type ?? DiskType.SSD,
|
||||
disk_interface: diskType?.disk_interface ?? DiskInterface.PCIe,
|
||||
created: new Date(),
|
||||
region: params.region,
|
||||
cost_plan,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { VmTemplate, LNVpsApi } from "../api";
|
||||
import { LNVpsApi, 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";
|
||||
|
||||
export default function HomePage() {
|
||||
const [offers, setOffers] = useState<Array<VmTemplate>>();
|
||||
const [offers, setOffers] = useState<VmTemplateResponse>();
|
||||
|
||||
useEffect(() => {
|
||||
const api = new LNVpsApi(ApiUrl, undefined);
|
||||
@ -21,14 +22,16 @@ export default function HomePage() {
|
||||
dedicated support, tailored to your needs.
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{offers?.map((a) => <VpsCard spec={a} key={a.id} />)}
|
||||
{offers !== undefined && offers.length === 0 && (
|
||||
{offers?.templates.map((a) => <VpsCard spec={a} key={a.id} />)}
|
||||
{offers?.templates !== undefined && offers.templates.length === 0 && (
|
||||
<div className="text-red-500 bold text-xl uppercase">
|
||||
No offers available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{offers?.custom_template && (
|
||||
<VpsCustomOrder templates={offers.custom_template} />
|
||||
)}
|
||||
<small className="text-neutral-400">
|
||||
All VPS come with 1x IPv4 and 1x IPv6 address and unmetered traffic
|
||||
</small>
|
||||
|
@ -31,12 +31,21 @@ export default function OrderPage() {
|
||||
setOrderError("");
|
||||
try {
|
||||
const ref = getRefCode();
|
||||
const newVm = await login.api.orderVm(
|
||||
template.id,
|
||||
useImage,
|
||||
useSshKey,
|
||||
ref?.code,
|
||||
);
|
||||
const newVm = template.pricing_id
|
||||
? await login.api.orderCustom(
|
||||
{
|
||||
cpu: template.cpu,
|
||||
memory: template.memory,
|
||||
disk: template.disk_size,
|
||||
disk_type: template.disk_type,
|
||||
disk_interface: template.disk_interface,
|
||||
pricing_id: template.pricing_id!,
|
||||
},
|
||||
useImage,
|
||||
useSshKey,
|
||||
ref?.code,
|
||||
)
|
||||
: await login.api.orderVm(template.id, useImage, useSshKey, ref?.code);
|
||||
clearRefCode();
|
||||
navigate("/vm/billing/renew", {
|
||||
state: newVm,
|
||||
|
@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
|
Reference in New Issue
Block a user