feat: sign-in nip7

This commit is contained in:
2024-09-23 13:28:45 +01:00
parent fb438c0dbc
commit eae46663d5
32 changed files with 596 additions and 361 deletions

21
src/components/button.tsx Normal file
View File

@ -0,0 +1,21 @@
import { forwardRef, HTMLProps } from "react";
export type AsyncButtonProps = {
onClick?: (e: React.MouseEvent) => Promise<void>;
} & Omit<HTMLProps<HTMLButtonElement>, "type" | "ref" | "onClick">;
const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>(
function AsyncButton(props, ref) {
return (
<button
ref={ref}
className="bg-slate-700 py-1 px-2 rounded-xl"
{...props}
>
{props.children}
</button>
);
},
);
export { AsyncButton };

View File

@ -1,19 +1,19 @@
import { GiB, KiB, MiB, TiB } from "../const"
import { GiB, KiB, MiB, TiB } from "../const";
interface BytesSizeProps {
value: number,
precision?: number
value: number;
precision?: number;
}
export default function BytesSize(props: BytesSizeProps) {
if (props.value >= TiB) {
return (props.value / TiB).toFixed(props.precision ?? 0) + "TB";
} else if (props.value >= GiB) {
return (props.value / GiB).toFixed(props.precision ?? 0) + "GB";
} else if (props.value >= MiB) {
return (props.value / MiB).toFixed(props.precision ?? 0) + "MB";
} else if (props.value >= KiB) {
return (props.value / KiB).toFixed(props.precision ?? 0) + "KB";
} else {
return (props.value).toFixed(props.precision ?? 0) + "B";
}
}
if (props.value >= TiB) {
return (props.value / TiB).toFixed(props.precision ?? 0) + "TB";
} else if (props.value >= GiB) {
return (props.value / GiB).toFixed(props.precision ?? 0) + "GB";
} else if (props.value >= MiB) {
return (props.value / MiB).toFixed(props.precision ?? 0) + "MB";
} else if (props.value >= KiB) {
return (props.value / KiB).toFixed(props.precision ?? 0) + "KB";
} else {
return props.value.toFixed(props.precision ?? 0) + "B";
}
}

View File

@ -1,14 +1,22 @@
import { CostInterval, MachineSpec } from "../api";
export default function CostLabel({ cost }: { cost: MachineSpec["cost"] }) {
function intervalName(n: number) {
switch (n) {
case CostInterval.Hour: return "Hour"
case CostInterval.Day: return "Day"
case CostInterval.Month: return "Month"
case CostInterval.Year: return "Year"
}
function intervalName(n: number) {
switch (n) {
case CostInterval.Hour:
return "Hour";
case CostInterval.Day:
return "Day";
case CostInterval.Month:
return "Month";
case CostInterval.Year:
return "Year";
}
}
return <>{cost.count} {cost.currency}/{intervalName(cost.interval)}</>
}
return (
<>
{cost.count} {cost.currency}/{intervalName(cost.interval)}
</>
);
}

View File

@ -0,0 +1,24 @@
import { SnortContext } from "@snort/system-react";
import { useContext } from "react";
import { AsyncButton } from "./button";
import { loginNip7 } from "../login";
import useLogin from "../hooks/login";
import Profile from "./profile";
import { NostrLink } from "@snort/system";
export default function LoginButton() {
const system = useContext(SnortContext);
const login = useLogin();
return !login ? (
<AsyncButton
onClick={async () => {
await loginNip7(system);
}}
>
Sign In
</AsyncButton>
) : (
<Profile link={NostrLink.publicKey(login.pubkey)} />
);
}

View File

@ -1,44 +1,64 @@
import { MachineSpec } from "../api";
import "./pay-button.css"
import "./pay-button.css";
declare global {
interface Window {
btcpay?: {
appendInvoiceFrame(invoiceId: string): void;
}
}
interface Window {
btcpay?: {
appendInvoiceFrame(invoiceId: string): void;
};
}
}
export default function VpsPayButton({ spec }: { spec: MachineSpec }) {
const serverUrl = "https://btcpay.v0l.io/api/v1/invoices";
const serverUrl = "https://btcpay.v0l.io/api/v1/invoices";
function handleFormSubmit(event: React.FormEvent) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200 && this.responseText) {
window.btcpay?.appendInvoiceFrame(JSON.parse(this.responseText).invoiceId);
}
};
xhttp.open('POST', serverUrl, true);
xhttp.send(new FormData(form));
}
function handleFormSubmit(event: React.FormEvent) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200 && this.responseText) {
window.btcpay?.appendInvoiceFrame(
JSON.parse(this.responseText).invoiceId,
);
}
};
xhttp.open("POST", serverUrl, true);
xhttp.send(new FormData(form));
}
if (!spec.active) {
return <div className="text-center text-xl uppercase bg-red-800 rounded-xl py-3 font-bold">
Unavailable
</div>
}
if (!spec.active) {
return (
<div className="text-center text-xl uppercase bg-red-800 rounded-xl py-3 font-bold">
Unavailable
</div>
);
}
return <form method="POST" action={serverUrl} className="btcpay-form btcpay-form--block" onSubmit={handleFormSubmit}>
<input type="hidden" name="storeId" value="CdaHy1puLx4kLC9BG3A9mu88XNyLJukMJRuuhAfbDrxg" />
<input type="hidden" name="jsonResponse" value="true" />
<input type="hidden" name="orderId" value={spec.id} />
<input type="hidden" name="price" value={spec.cost.count} />
<input type="hidden" name="currency" value={spec.cost.currency} />
<input type="image" className="submit" name="submit" src="https://btcpay.v0l.io/img/paybutton/pay.svg"
alt="Pay with BTCPay Server, a Self-Hosted Bitcoin Payment Processor" />
return (
<form
method="POST"
action={serverUrl}
className="btcpay-form btcpay-form--block w-full"
onSubmit={handleFormSubmit}
>
<input
type="hidden"
name="storeId"
value="CdaHy1puLx4kLC9BG3A9mu88XNyLJukMJRuuhAfbDrxg"
/>
<input type="hidden" name="jsonResponse" value="true" />
<input type="hidden" name="orderId" value={spec.id} />
<input type="hidden" name="price" value={spec.cost.count} />
<input type="hidden" name="currency" value={spec.cost.currency} />
<input
type="image"
className="w-full"
name="submit"
src="https://btcpay.v0l.io/img/paybutton/pay.svg"
alt="Pay with BTCPay Server, a Self-Hosted Bitcoin Payment Processor"
/>
</form>
}
);
}

View File

@ -3,11 +3,18 @@ import { NostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
export default function Profile({ link }: { link: NostrLink }) {
const profile = useUserProfile(link.id);
return <div className="flex gap-2 items-center">
<img src={profile?.picture} className="w-12 h-12 rounded-full bg-neutral-500" />
<div>
{profile?.display_name ?? profile?.name ?? hexToBech32("npub", link.id).slice(0, 12)}
</div>
const profile = useUserProfile(link.id);
return (
<div className="flex gap-2 items-center">
<img
src={profile?.picture}
className="w-12 h-12 rounded-full bg-neutral-500"
/>
<div>
{profile?.display_name ??
profile?.name ??
hexToBech32("npub", link.id).slice(0, 12)}
</div>
</div>
}
);
}

View File

@ -4,14 +4,23 @@ import CostLabel from "./cost";
import VpsPayButton from "./pay-button";
export default function VpsCard({ spec }: { spec: MachineSpec }) {
return <div className="rounded-xl border border-neutral-600 px-3 py-2">
<h2>{spec.id}</h2>
<ul>
<li>CPU: {spec.cpu}vCPU</li>
<li>RAM: <BytesSize value={spec.ram} /></li>
<li>{spec.disk.type === DiskType.SSD ? "SSD" : "HDD"}: <BytesSize value={spec.disk.size} /></li>
</ul>
<h2><CostLabel cost={spec.cost} /></h2>
<VpsPayButton spec={spec} />
return (
<div className="rounded-xl border border-neutral-600 px-3 py-2">
<h2>{spec.id}</h2>
<ul>
<li>CPU: {spec.cpu}vCPU</li>
<li>
RAM: <BytesSize value={spec.ram} />
</li>
<li>
{spec.disk.type === DiskType.SSD ? "SSD" : "HDD"}:{" "}
<BytesSize value={spec.disk.size} />
</li>
</ul>
<h2>
<CostLabel cost={spec.cost} />
</h2>
<VpsPayButton spec={spec} />
</div>
}
);
}