feat: currency selector
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@ -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
4
.env
@ -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=""
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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),
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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],
|
||||||
);
|
);
|
||||||
|
11
src/login.ts
11
src/login.ts
@ -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));
|
||||||
|
@ -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>
|
||||||
|
Reference in New Issue
Block a user