From 8e3e4c036494c6690ce64aea5d7fc38f5e23380a Mon Sep 17 00:00:00 2001 From: kieran Date: Thu, 6 Mar 2025 21:43:31 +0000 Subject: [PATCH] feat: custom pricing --- src/api.ts | 85 +++++++++++++++++++++-- src/components/cost.tsx | 3 +- src/components/pay-button.tsx | 6 +- src/components/vps-custom.tsx | 122 ++++++++++++++++++++++++++++++++++ src/pages/home.tsx | 13 ++-- src/pages/order.tsx | 21 ++++-- tsconfig.app.json | 2 +- 7 files changed, 228 insertions(+), 24 deletions(-) create mode 100644 src/components/vps-custom.tsx diff --git a/src/api.ts b/src/api.ts index c301d42..72039b5 100644 --- a/src/api.ts +++ b/src/api.ts @@ -9,6 +9,17 @@ export type ApiResponse = 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; +} + +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; + custom_template?: Array; +} + 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>>( - await this.#req("/api/v1/vm/templates", "GET"), - ); + const { data } = await this.#handleResponse< + ApiResponse + >(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>( + 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>( + 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>( await this.#req(`/api/v1/vm/${vm_id}/renew`, "GET"), diff --git a/src/components/cost.tsx b/src/components/cost.tsx index 7a1596d..b726dae 100644 --- a/src/components/cost.tsx +++ b/src/components/cost.tsx @@ -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)} ); } diff --git a/src/components/pay-button.tsx b/src/components/pay-button.tsx index 25c3bc6..01dbba4 100644 --- a/src/components/pay-button.tsx +++ b/src/components/pay-button.tsx @@ -21,11 +21,7 @@ export default function VpsPayButton({ spec }: { spec: VmTemplate }) { return ( - navigte("/login", { - state: spec, - }) - } + onClick={() => navigte("/login")} > Login To Order diff --git a/src/components/vps-custom.tsx b/src/components/vps-custom.tsx new file mode 100644 index 0000000..d52670b --- /dev/null +++ b/src/components/vps-custom.tsx @@ -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; +}) { + 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(); + + 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 ( +
+
Custom VPS Order
+
+
{cpu} CPU
+ setCpu(e.target.valueAsNumber)} + min={params.min_cpu} + max={params.max_cpu} + step={1} + className="grow" + /> +
+
+
{ram.toString()} GB RAM
+ setRam(e.target.valueAsNumber)} + min={Math.floor(params.min_memory / GiB)} + max={Math.floor(params.max_memory / GiB)} + step={1} + className="grow" + /> +
+
+
+ {disk.toString()} GB {diskType?.disk_type.toLocaleUpperCase()} +
+ setDisk(e.target.valueAsNumber)} + min={Math.floor(params.min_disk / GiB)} + max={Math.floor(params.max_disk / GiB)} + step={1} + className="grow" + /> +
+ {price && ( +
+
+ +
+
+ +
+
+ )} +
+ ); +} diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 2562d82..5c97112 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -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>(); + const [offers, setOffers] = useState(); useEffect(() => { const api = new LNVpsApi(ApiUrl, undefined); @@ -21,14 +22,16 @@ export default function HomePage() { dedicated support, tailored to your needs.
- {offers?.map((a) => )} - {offers !== undefined && offers.length === 0 && ( + {offers?.templates.map((a) => )} + {offers?.templates !== undefined && offers.templates.length === 0 && (
No offers available
)}
- + {offers?.custom_template && ( + + )} All VPS come with 1x IPv4 and 1x IPv6 address and unmetered traffic diff --git a/src/pages/order.tsx b/src/pages/order.tsx index d4c87e2..70e4323 100644 --- a/src/pages/order.tsx +++ b/src/pages/order.tsx @@ -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, diff --git a/tsconfig.app.json b/tsconfig.app.json index 9ef45e0..8572c7a 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true,