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:
|
||||
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
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 {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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],
|
||||
);
|
||||
|
11
src/login.ts
11
src/login.ts
@ -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));
|
||||
|
@ -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>
|
||||
|
Reference in New Issue
Block a user