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

View File

@ -51,7 +51,7 @@ export default function Modal(props: ModalProps) {
className={
props.bodyClassName ??
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-[50vh]": !(props.ready ?? true),

View File

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

View File

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

View File

@ -11,6 +11,7 @@ export interface LoginSession {
publicKey: string;
privateKey?: string;
bunker?: string;
currency: string;
}
class LoginStore extends ExternalStore<LoginSession | undefined> {
#session?: LoginSession;
@ -42,6 +43,7 @@ class LoginStore extends ExternalStore<LoginSession | undefined> {
this.#session = {
type: type ?? "nip7",
publicKey: pubkey,
currency: "EUR"
};
this.#save();
}
@ -52,6 +54,7 @@ class LoginStore extends ExternalStore<LoginSession | undefined> {
type: "nsec",
publicKey: s.getPubKey(),
privateKey: key,
currency: "EUR"
};
this.#save();
}
@ -62,6 +65,7 @@ class LoginStore extends ExternalStore<LoginSession | undefined> {
publicKey: remotePubkey,
privateKey: localKey,
bunker: url,
currency: "EUR"
};
this.#save();
}
@ -99,6 +103,13 @@ class LoginStore extends ExternalStore<LoginSession | undefined> {
throw "Signer not setup!";
}
updateSession(fx: (s: LoginSession) => void) {
if (this.#session) {
fx(this.#session);
this.#save();
}
}
#save() {
if (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 { FilterButton } from "../components/button-filter";
import { dedupe } from "@snort/shared";
import useLogin from "../hooks/login";
export default function HomePage() {
const login = useLogin();
const [offers, setOffers] = useState<VmTemplateResponse>();
const [region, setRegion] = useState<Array<number>>([]);
@ -66,7 +68,7 @@ export default function HomePage() {
<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.
</small>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4">
<div className="text-center">
<a href="/lnvps.asc">PGP</a>
{" | "}
@ -91,12 +93,19 @@ export default function HomePage() {
Speedtest
</a>
</div>
<div className="text-xs text-center text-neutral-400">
LNVPS is a trading name of Apex Strata Ltd, a company registered in
Ireland.
<br />
Comany Number: 702423, Address: Suite 10628, 26/27 Upper Pembroke
Street, Dublin 2, D02 X361, Ireland
{import.meta.env.VITE_FOOTER_NOTE_1 && <div className="text-xs text-center text-neutral-400">
{import.meta.env.VITE_FOOTER_NOTE_1}
</div>}
{import.meta.env.VITE_FOOTER_NOTE_2 && <div className="text-xs text-center text-neutral-400">
{import.meta.env.VITE_FOOTER_NOTE_2}
</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>