feat: currency selector
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-03-28 14:29:00 +00:00
parent c67dd4c793
commit 9d70de9b8a
8 changed files with 91 additions and 47 deletions

View File

@ -13,6 +13,8 @@ steps:
environment: environment:
TOKEN: TOKEN:
from_secret: registry_token from_secret: registry_token
VITE_FOOTER_NOTE_1: "LNVPS is a trading name of Apex Strata Ltd, a company registered in Ireland."
VITE_FOOTER_NOTE_2: "Comany Number: 702423, Address: Suite 10628, 26/27 Upper Pembroke Street, Dublin 2, D02 X361, Ireland"
commands: commands:
- dockerd & - dockerd &
- docker login -u registry -p $TOKEN registry.v0l.io - docker login -u registry -p $TOKEN registry.v0l.io

4
.env
View File

@ -1 +1,3 @@
VITE_API_URL="https://api.lnvps.net" VITE_API_URL="https://api.lnvps.net"
VITE_FOOTER_NOTE_1=""
VITE_FOOTER_NOTE_2=""

View File

@ -1,41 +1,57 @@
import useLogin from "../hooks/login";
interface Price { interface Price {
currency: string; currency: string;
amount: number; amount: number;
} }
type Cost = Price & { interval_type?: string; other_price?: Array<Price> }; type Cost = Price & { interval_type?: string };
export default function CostLabel({ export default function CostLabel({ cost }: { cost: Cost & { other_price?: Array<Price> } }) {
cost, const login = useLogin();
converted,
}: { if (cost.currency === login?.currency) {
cost: Cost; return <CostAmount cost={cost} converted={false} />
converted?: boolean; } else {
}) { const converted_price = cost.other_price?.find((p) => p.currency === login?.currency);
function intervalName(n: string) { if (converted_price) {
switch (n) { return <div>
case "day": <CostAmount cost={{
return "Day"; ...converted_price,
case "month": interval_type: cost.interval_type
return "Month"; }} converted={true} />
case "year": <CostAmount cost={cost} converted={false} className="text-sm text-neutral-400" />
return "Year"; </div>
} else {
return <CostAmount cost={cost} converted={false} />
} }
} }
}
function intervalName(n: string) {
switch (n) {
case "day":
return "Day";
case "month":
return "Month";
case "year":
return "Year";
}
}
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'
});
return ( return (
<div> <div className={className}>
{converted && "~"} {converted && "~"}
{cost.currency !== "BTC" {cost.currency !== "BTC"
? cost.amount.toFixed(2) ? formatter.format(cost.amount)
: Math.floor(cost.amount * 1e8).toLocaleString()}{" "} : Math.floor(cost.amount * 1e8).toLocaleString()}
{cost.currency === "BTC" ? "sats" : cost.currency} {cost.currency === "BTC" && " sats"}
{cost.interval_type && <>/{intervalName(cost.interval_type)}</>} {cost.interval_type && <>/{intervalName(cost.interval_type)}</>}
{cost.other_price &&
cost.other_price.map((a) => (
<div key={a.currency} className="text-xs">
<CostLabel cost={a} converted={true} />
</div>
))}
</div> </div>
); );
} }

View File

@ -51,7 +51,7 @@ export default function Modal(props: ModalProps) {
className={ className={
props.bodyClassName ?? props.bodyClassName ??
classNames( classNames(
"relative bg-neutral-700 p-8 transition max-xl:rounded-t-3xl xl:rounded-3xl max-xl:mt-auto xl:my-auto max-lg:w-full", "relative bg-neutral-700 p-8 transition max-xl:rounded-t-3xl lg:rounded-3xl max-xl:mt-auto lg:my-auto max-lg:w-full",
{ {
"max-xl:-translate-y-[calc(100vh-100dvh)]": props.ready ?? true, "max-xl:-translate-y-[calc(100vh-100dvh)]": props.ready ?? true,
"max-xl:translate-y-[50vh]": !(props.ready ?? true), "max-xl:translate-y-[50vh]": !(props.ready ?? true),

View File

@ -5,10 +5,12 @@ import VpsPayButton from "./pay-button";
export default function VpsCard({ spec }: { spec: VmTemplate }) { export default function VpsCard({ spec }: { spec: VmTemplate }) {
return ( return (
<div className="rounded-xl border border-neutral-600 px-3 py-2"> <div className="rounded-xl border border-neutral-600 px-3 py-2 flex flex-col gap-2">
<h2>{spec.name}</h2> <div className="text-xl">{spec.name}</div>
<ul> <ul>
<li>CPU: {spec.cpu}vCPU</li> <li>
CPU: {spec.cpu}vCPU
</li>
<li> <li>
RAM: <BytesSize value={spec.memory} /> RAM: <BytesSize value={spec.memory} />
</li> </li>
@ -17,7 +19,7 @@ export default function VpsCard({ spec }: { spec: VmTemplate }) {
</li> </li>
<li>Location: {spec.region?.name}</li> <li>Location: {spec.region?.name}</li>
</ul> </ul>
<h2>{spec.cost_plan && <CostLabel cost={spec.cost_plan} />}</h2> <div className="text-lg">{spec.cost_plan && <CostLabel cost={spec.cost_plan} />}</div>
<VpsPayButton spec={spec} /> <VpsPayButton spec={spec} />
</div> </div>
); );

View File

@ -1,5 +1,5 @@
import { useContext, useMemo, useSyncExternalStore } from "react"; import { useContext, useMemo, useSyncExternalStore } from "react";
import { LoginState } from "../login"; import { LoginSession, LoginState } from "../login";
import { SnortContext } from "@snort/system-react"; import { SnortContext } from "@snort/system-react";
import { LNVpsApi } from "../api"; import { LNVpsApi } from "../api";
import { ApiUrl } from "../const"; import { ApiUrl } from "../const";
@ -14,12 +14,14 @@ export default function useLogin() {
() => () =>
session session
? { ? {
type: session.type, type: session.type,
publicKey: session.publicKey, publicKey: session.publicKey,
system, system,
api: new LNVpsApi(ApiUrl, LoginState.getSigner()), currency: session.currency,
logout: () => LoginState.logout(), api: new LNVpsApi(ApiUrl, LoginState.getSigner()),
} update: (fx: (ses: LoginSession) => void) => LoginState.updateSession(fx),
logout: () => LoginState.logout(),
}
: undefined, : undefined,
[session, system], [session, system],
); );

View File

@ -11,6 +11,7 @@ export interface LoginSession {
publicKey: string; publicKey: string;
privateKey?: string; privateKey?: string;
bunker?: string; bunker?: string;
currency: string;
} }
class LoginStore extends ExternalStore<LoginSession | undefined> { class LoginStore extends ExternalStore<LoginSession | undefined> {
#session?: LoginSession; #session?: LoginSession;
@ -42,6 +43,7 @@ class LoginStore extends ExternalStore<LoginSession | undefined> {
this.#session = { this.#session = {
type: type ?? "nip7", type: type ?? "nip7",
publicKey: pubkey, publicKey: pubkey,
currency: "EUR"
}; };
this.#save(); this.#save();
} }
@ -52,6 +54,7 @@ class LoginStore extends ExternalStore<LoginSession | undefined> {
type: "nsec", type: "nsec",
publicKey: s.getPubKey(), publicKey: s.getPubKey(),
privateKey: key, privateKey: key,
currency: "EUR"
}; };
this.#save(); this.#save();
} }
@ -62,6 +65,7 @@ class LoginStore extends ExternalStore<LoginSession | undefined> {
publicKey: remotePubkey, publicKey: remotePubkey,
privateKey: localKey, privateKey: localKey,
bunker: url, bunker: url,
currency: "EUR"
}; };
this.#save(); this.#save();
} }
@ -99,6 +103,13 @@ class LoginStore extends ExternalStore<LoginSession | undefined> {
throw "Signer not setup!"; throw "Signer not setup!";
} }
updateSession(fx: (s: LoginSession) => void) {
if (this.#session) {
fx(this.#session);
this.#save();
}
}
#save() { #save() {
if (this.#session) { if (this.#session) {
window.localStorage.setItem("session", JSON.stringify(this.#session)); window.localStorage.setItem("session", JSON.stringify(this.#session));

View File

@ -7,8 +7,10 @@ import { VpsCustomOrder } from "../components/vps-custom";
import { LatestNews } from "../components/latest-news"; import { LatestNews } from "../components/latest-news";
import { FilterButton } from "../components/button-filter"; import { FilterButton } from "../components/button-filter";
import { dedupe } from "@snort/shared"; import { dedupe } from "@snort/shared";
import useLogin from "../hooks/login";
export default function HomePage() { export default function HomePage() {
const login = useLogin();
const [offers, setOffers] = useState<VmTemplateResponse>(); const [offers, setOffers] = useState<VmTemplateResponse>();
const [region, setRegion] = useState<Array<number>>([]); const [region, setRegion] = useState<Array<number>>([]);
@ -66,7 +68,7 @@ export default function HomePage() {
<small className="text-neutral-400 text-center"> <small className="text-neutral-400 text-center">
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.
</small> </small>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-4">
<div className="text-center"> <div className="text-center">
<a href="/lnvps.asc">PGP</a> <a href="/lnvps.asc">PGP</a>
{" | "} {" | "}
@ -91,12 +93,19 @@ export default function HomePage() {
Speedtest Speedtest
</a> </a>
</div> </div>
<div className="text-xs text-center text-neutral-400"> {import.meta.env.VITE_FOOTER_NOTE_1 && <div className="text-xs text-center text-neutral-400">
LNVPS is a trading name of Apex Strata Ltd, a company registered in {import.meta.env.VITE_FOOTER_NOTE_1}
Ireland. </div>}
<br /> {import.meta.env.VITE_FOOTER_NOTE_2 && <div className="text-xs text-center text-neutral-400">
Comany Number: 702423, Address: Suite 10628, 26/27 Upper Pembroke {import.meta.env.VITE_FOOTER_NOTE_2}
Street, Dublin 2, D02 X361, Ireland </div>}
<div className="text-sm text-center">
Currency:
{" "}
<select value={login?.currency ?? "EUR"}
onChange={(e) => login?.update(s => s.currency = e.target.value)}>
{["BTC", "EUR", "USD", "GBP", "AUD", "CAD", "CHF", "JPY"].map((a) => <option>{a}</option>)}
</select>
</div> </div>
</div> </div>
</div> </div>