diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 503f6d1..4a96a52 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { createEffect, Show } from "solid-js" import { Router, Route, useLocation, useNavigate } from "@solidjs/router" import type { Component } from "solid-js" import AppShell from "@/components/AppShell" +import Toast from "@/components/Toast" import Home from "@/pages/Home" import RelayList from "@/pages/relays/RelayList" import RelayNew from "@/pages/relays/RelayNew" @@ -31,6 +32,7 @@ function Layout(props: { children?: any }) { {props.children}}> {props.children} + ) } diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 7466802..34306ea 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -7,7 +7,7 @@ export default function Navbar() { const picture = useProfilePicture(() => account()?.pubkey) return ( - + diff --git a/frontend/src/components/PricingTable.tsx b/frontend/src/components/PricingTable.tsx index 76bd348..0875dd0 100644 --- a/frontend/src/components/PricingTable.tsx +++ b/frontend/src/components/PricingTable.tsx @@ -1,6 +1,6 @@ import { A } from "@solidjs/router" import { For } from "solid-js" -import { RELAY_PLANS, type RelayPlanId } from "@/lib/relayPlans" +import { PLANS, type PlanId } from "@/lib/api" function CheckIcon() { return ( @@ -22,8 +22,8 @@ function XIcon() { type PricingTableProps = { selectable?: boolean - selectedPlan?: RelayPlanId - onSelect?: (plan: RelayPlanId) => void + selectedPlan?: PlanId + onSelect?: (plan: PlanId) => void ctaHref?: string compactOnMobile?: boolean } @@ -31,7 +31,7 @@ type PricingTableProps = { export default function PricingTable(props: PricingTableProps) { return ( - + {(plan) => { const isPopular = plan.id === "basic" const isSelected = () => props.selectable && props.selectedPlan === plan.id diff --git a/frontend/src/components/RelayDetailCard.tsx b/frontend/src/components/RelayDetailCard.tsx index 1d7fab6..fb577e3 100644 --- a/frontend/src/components/RelayDetailCard.tsx +++ b/frontend/src/components/RelayDetailCard.tsx @@ -1,10 +1,8 @@ import { A } from "@solidjs/router" import { Show, createEffect, createSignal, onCleanup } from "solid-js" -import type { Relay } from "@/lib/api" +import type { Relay, PlanId } from "@/lib/api" import menuDotsIcon from "@/assets/menu-dots-2.svg" -import Modal from "@/components/Modal" import PricingTable from "@/components/PricingTable" -import { RELAY_PLAN_IDS, type RelayPlanId } from "@/lib/relayPlans" function Field(props: { label: string; children: any }) { return ( @@ -89,8 +87,7 @@ type RelayDetailCardProps = { onToggleMediaStorage?: () => void onToggleLivekitSupport?: () => void onTogglePushNotifications?: () => void - onUpdatePlan?: (plan: RelayPlanId) => Promise - updatingPlan?: boolean + onUpdatePlan?: (plan: PlanId) => Promise enforcePlanLimits?: boolean showPlanActions?: boolean } @@ -103,10 +100,8 @@ export default function RelayDetailCard(props: RelayDetailCardProps) { return fallback } const [menuOpen, setMenuOpen] = createSignal(false) - const [planModalOpen, setPlanModalOpen] = createSignal(false) - const [selectedPlan, setSelectedPlan] = createSignal("free") - const [planError, setPlanError] = createSignal("") - const [submittingPlan, setSubmittingPlan] = createSignal(false) + const [plan, setPlan] = createSignal(props.relay.plan) + let menuContainerRef: HTMLDivElement | undefined const memberLimitByPlan: Record = { @@ -116,32 +111,12 @@ export default function RelayDetailCard(props: RelayDetailCardProps) { } const memberLimitLabel = () => memberLimitByPlan[r().plan] ?? "?" - const isTopTier = () => r().plan === "growth" const planLimited = () => (props.enforcePlanLimits ?? true) && r().plan === "free" const showPlanActions = () => props.showPlanActions ?? true - createEffect(() => { - if (!planModalOpen()) return - const current = RELAY_PLAN_IDS.find((id) => id === r().plan) ?? "free" - setSelectedPlan(current) - setPlanError("") - }) - - const canSubmitPlan = () => selectedPlan() !== r().plan - - async function handlePlanContinue() { - if (!props.onUpdatePlan || submittingPlan() || !canSubmitPlan()) return - setPlanError("") - setSubmittingPlan(true) - - try { - await props.onUpdatePlan(selectedPlan()) - setPlanModalOpen(false) - } catch (e) { - setPlanError(e instanceof Error ? e.message : "Failed to update plan") - } finally { - setSubmittingPlan(false) - } + async function changePlan(plan: PlanId) { + setPlan(plan) + props.onUpdatePlan?.(plan) } createEffect(() => { @@ -286,13 +261,9 @@ export default function RelayDetailCard(props: RelayDetailCardProps) { } > - setPlanModalOpen(true)} - > + Update Plan - + } @@ -319,13 +290,9 @@ export default function RelayDetailCard(props: RelayDetailCardProps) { } > - setPlanModalOpen(true)} - > + Update Plan - + } @@ -352,69 +319,31 @@ export default function RelayDetailCard(props: RelayDetailCardProps) { {r().tenant} - - - } - > - - Upgrade Plan - - - } - > - setPlanModalOpen(true)} - > - Update Plan - - - - - setPlanModalOpen(false)} - wrapperClass="fixed inset-0 z-40 flex items-center justify-center bg-black/40 p-4" - panelClass="w-full max-w-5xl max-h-[90vh] overflow-y-auto rounded-2xl bg-white" - > - - Update plan - Choose the plan you want for this relay. + + - - - - {planError()} + + + {r().plan} + + } + > + + + - - - void handlePlanContinue()} - disabled={!canSubmitPlan() || submittingPlan() || !!props.updatingPlan} - class="w-full py-3 rounded-xl bg-blue-600 text-white font-semibold hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" - > - {submittingPlan() || props.updatingPlan ? "Updating..." : "Continue"} - - - - + + ) } diff --git a/frontend/src/components/RelayForm.tsx b/frontend/src/components/RelayForm.tsx index 0916557..2bed481 100644 --- a/frontend/src/components/RelayForm.tsx +++ b/frontend/src/components/RelayForm.tsx @@ -2,6 +2,7 @@ import { createEffect, createSignal } from "solid-js" import type { Relay } from "@/lib/hooks" import { slugify } from "@/lib/slugify" import { PLANS } from "@/lib/api" +import { setToastMessage } from "@/components/Toast" export type RelayFormValues = Pick @@ -14,17 +15,22 @@ type RelayFormProps = { } export default function RelayForm(props: RelayFormProps) { - const [plan, setPlan] = createSignal(props.initialValues?.plan ?? "") + const [plan, setPlan] = createSignal(props.initialValues?.plan ?? PLANS[0].id) const [name, setName] = createSignal(props.initialValues?.info_name ?? "") const [subdomain, setSubdomain] = createSignal(props.initialValues?.subdomain ?? "") const [icon, setIcon] = createSignal(props.initialValues?.info_icon ?? "") const [description, setDescription] = createSignal(props.initialValues?.info_description ?? "") const [submitting, setSubmitting] = createSignal(false) - const [error, setError] = createSignal("") async function handleSubmit(e: Event) { e.preventDefault() - setError("") + + if (!plan()) { + setToastMessage("Please select a plan") + return + } + + setToastMessage("") setSubmitting(true) try { @@ -36,7 +42,7 @@ export default function RelayForm(props: RelayFormProps) { info_description: description(), }) } catch (e) { - setError(e instanceof Error ? e.message : "Failed to save relay") + setToastMessage(e instanceof Error ? e.message : "Failed to save relay") } finally { setSubmitting(false) } @@ -106,12 +112,11 @@ export default function RelayForm(props: RelayFormProps) { {p.price === 0 ? "Free" : `${p.price.toLocaleString()} sats/mo`} - {p.members} members + {p.memberLabel} ))} - {error() && {error()}} void -} +export const [toastMessage, setToastMessage] = createSignal("") -export default function Toast(props: ToastProps) { +export default function Toast() { const [visible, setVisible] = createSignal(false) let hideTimer: number | undefined @@ -26,7 +22,7 @@ export default function Toast(props: ToastProps) { } createEffect(() => { - const message = props.message?.trim() + const message = toastMessage()?.trim() clearTimers() if (!message) { @@ -46,11 +42,11 @@ export default function Toast(props: ToastProps) { hideTimer = window.setTimeout(() => { setVisible(false) clearTimer = window.setTimeout(() => { - props.onClear?.() + setToastMessage("") clearTimer = undefined }, 250) hideTimer = undefined - }, props.duration ?? 10_000) + }, 10_000) }) onCleanup(() => { @@ -58,7 +54,7 @@ export default function Toast(props: ToastProps) { }) return ( - + - {props.message} + {toastMessage()} ) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 7940987..a5eb4a5 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -32,9 +32,36 @@ export class ApiError extends Error { export type Plan = Record export const PLANS = [ - { id: "free", label: "Free", price: 0, members: "Up to 10", blossom: false, livekit: false }, - { id: "basic", label: "Basic", price: 10_000, members: "Up to 100", blossom: true, livekit: true }, - { id: "growth", label: "Growth", price: 50_000, members: "Unlimited", blossom: true, livekit: true }, + { + id: "free", + label: "Free", + subtitle: "Get started, no commitment.", + price: 0, + priceLabel: "0", + memberLabel: "Up to 10 members", + blossom: false, + livekit: false, + }, + { + id: "basic", + label: "Basic", + subtitle: "For growing communities.", + price: 10_000, + priceLabel: "10K", + memberLabel: "Up to 100 members", + blossom: true, + livekit: true, + }, + { + id: "growth", + label: "Growth", + subtitle: "For large-scale communities.", + price: 50_000, + priceLabel: "50K", + memberLabel: "Unlimited members", + blossom: true, + livekit: true, + }, ] as const export type PlanId = (typeof PLANS)[number]["id"] @@ -44,7 +71,7 @@ export type Relay = { tenant: string schema: string subdomain: string - plan: string + plan: PlanId status: string sync_error: string info_name: string diff --git a/frontend/src/lib/relayPlans.ts b/frontend/src/lib/relayPlans.ts deleted file mode 100644 index 6acfed6..0000000 --- a/frontend/src/lib/relayPlans.ts +++ /dev/null @@ -1,45 +0,0 @@ -export const RELAY_PLAN_IDS = ["free", "basic", "growth"] as const - -export type RelayPlanId = (typeof RELAY_PLAN_IDS)[number] - -export const RELAY_PLANS = [ - { - id: "free", - label: "Free", - subtitle: "Get started, no commitment.", - price: 0, - priceLabel: "0", - memberLabel: "Up to 10 members", - blossom: false, - livekit: false, - }, - { - id: "basic", - label: "Basic", - subtitle: "For growing communities.", - price: 10_000, - priceLabel: "10K", - memberLabel: "Up to 100 members", - blossom: true, - livekit: true, - }, - { - id: "growth", - label: "Growth", - subtitle: "For large-scale communities.", - price: 50_000, - priceLabel: "50K", - memberLabel: "Unlimited members", - blossom: true, - livekit: true, - }, -] as const satisfies readonly { - id: RelayPlanId - label: string - subtitle: string - price: number - priceLabel: string - memberLabel: string - blossom: boolean - livekit: boolean -}[] diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 4412a0e..30bd217 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -22,6 +22,7 @@ export default function Home() { navigate(`/relays/${relay.id}`) } else { setDraftRelay(values) + setShowRelayModal(false) setShowLoginModal(true) } } diff --git a/frontend/src/pages/relays/RelayDetail.tsx b/frontend/src/pages/relays/RelayDetail.tsx index 5f72d19..547a98c 100644 --- a/frontend/src/pages/relays/RelayDetail.tsx +++ b/frontend/src/pages/relays/RelayDetail.tsx @@ -1,6 +1,6 @@ import { useParams } from "@solidjs/router" import { createMemo, createResource, createSignal, Show } from "solid-js" -import type { RelayPlanId } from "@/lib/relayPlans" +import type { PlanId } from "@/lib/api" import BackLink from "@/components/BackLink" import PageContainer from "@/components/PageContainer" import RelayDetailCard from "@/components/RelayDetailCard" @@ -131,7 +131,7 @@ export default function RelayDetail() { void updateRelay(next, current) } - async function handleUpdatePlan(plan: RelayPlanId) { + async function handleUpdatePlan(plan: PlanId) { const current = relay() if (!current) return diff --git a/frontend/src/pages/relays/RelayNew.tsx b/frontend/src/pages/relays/RelayNew.tsx index ee5a6ba..ac26429 100644 --- a/frontend/src/pages/relays/RelayNew.tsx +++ b/frontend/src/pages/relays/RelayNew.tsx @@ -138,7 +138,7 @@ export default function RelayNew() { {p.price === 0 ? "Free" : `${p.price.toLocaleString()} sats/mo`} - {p.members} members + {p.memberLabel} ))} diff --git a/frontend/src/views/Login.tsx b/frontend/src/views/Login.tsx index 9972765..910d700 100644 --- a/frontend/src/views/Login.tsx +++ b/frontend/src/views/Login.tsx @@ -5,6 +5,7 @@ import { PasswordSigner } from "applesauce-signers" import QrScanner from "qr-scanner" import QRCode from "qrcode" import { accountManager, identity, PLATFORM_NAME } from "@/lib/state" +import useMinLoading from "@/components/useMinLoading" const NIP46_RELAYS = ['wss://bucket.coracle.social', 'wss://ephemeral.snowflare.cc'] @@ -46,7 +47,8 @@ type LoginPageProps = LoginProps & Partial> export default function Login(props: LoginPageProps = {}) { const navigate = useNavigate() const [tab, setTab] = createSignal(window.nostr ? "nip07" : "nip46") - const [loading, setLoading] = createSignal(false) + const [rawLoading, setRawLoading] = createSignal(false) + const loading = useMinLoading(() => rawLoading()) const [error, setError] = createSignal("") const [screen, setScreen] = createSignal("tabs") @@ -72,19 +74,19 @@ export default function Login(props: LoginPageProps = {}) { async function loginWithNip07() { setError("") - setLoading(true) + setRawLoading(true) try { await completeLogin(await ExtensionAccount.fromExtension()) } catch (e) { setError(e instanceof Error ? e.message : "Failed to login with extension") } finally { - setLoading(false) + setRawLoading(false) } } async function startNostrConnect() { setError("") - setLoading(true) + setRawLoading(true) try { const NostrConnectSigner = await loadNostrConnectSigner() @@ -110,13 +112,13 @@ export default function Login(props: LoginPageProps = {}) { } catch (e) { setError(e instanceof Error ? e.message : "Failed to connect signer") } finally { - setLoading(false) + setRawLoading(false) } } async function loginWithBunker() { setError("") - setLoading(true) + setRawLoading(true) try { const uri = normalizeBunkerUrl(bunkerUrl()) const NostrConnectSigner = await loadNostrConnectSigner() @@ -127,13 +129,13 @@ export default function Login(props: LoginPageProps = {}) { } catch (e) { setError(e instanceof Error ? e.message : "Invalid bunker URL") } finally { - setLoading(false) + setRawLoading(false) } } async function loginWithKeyMaterial() { setError("") - setLoading(true) + setRawLoading(true) try { if (ncryptsecValue().trim()) { if (!password().trim()) { @@ -153,7 +155,7 @@ export default function Login(props: LoginPageProps = {}) { } catch (e) { setError(e instanceof Error ? e.message : "Invalid key") } finally { - setLoading(false) + setRawLoading(false) } }
Choose the plan you want for this relay.
{planError()}
{error()}