More stuff

This commit is contained in:
Jon Staab
2026-03-27 14:08:05 -07:00
parent bc45017222
commit 77ea366c69
12 changed files with 103 additions and 186 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ export default function Navbar() {
const picture = useProfilePicture(() => account()?.pubkey)
return (
<nav class="bg-white border-b border-gray-200">
<nav class="fixed inset-0 h-screen bg-white border-b border-gray-200 z-40">
<div class="max-w-4xl mx-auto px-4 h-14 flex items-center justify-between">
<A href={account() ? "/relays" : "/"} class="flex items-center gap-2">
<img src="/caravel.png" alt={PLATFORM_NAME} class="h-8 w-8 rounded-full object-cover" />
+4 -4
View File
@@ -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 (
<div class={`grid items-start ${props.compactOnMobile ? "grid-cols-3 gap-2 sm:grid-cols-1 sm:gap-6 md:grid-cols-3" : "grid-cols-1 md:grid-cols-3 gap-6"}`}>
<For each={RELAY_PLANS}>
<For each={PLANS}>
{(plan) => {
const isPopular = plan.id === "basic"
const isSelected = () => props.selectable && props.selectedPlan === plan.id
+32 -103
View File
@@ -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<void>
updatingPlan?: boolean
onUpdatePlan?: (plan: PlanId) => Promise<void>
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<RelayPlanId>("free")
const [planError, setPlanError] = createSignal("")
const [submittingPlan, setSubmittingPlan] = createSignal(false)
const [plan, setPlan] = createSignal<PlanId>(props.relay.plan)
let menuContainerRef: HTMLDivElement | undefined
const memberLimitByPlan: Record<string, string> = {
@@ -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) {
</A>
}
>
<button
type="button"
class="inline-flex items-center rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700 hover:bg-blue-100"
onClick={() => setPlanModalOpen(true)}
>
<span class="inline-flex items-center rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700">
Update Plan
</button>
</span>
</Show>
</Show>
}
@@ -319,13 +290,9 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
</A>
}
>
<button
type="button"
class="inline-flex items-center rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700 hover:bg-blue-100"
onClick={() => setPlanModalOpen(true)}
>
<span class="inline-flex items-center rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700">
Update Plan
</button>
</span>
</Show>
</Show>
}
@@ -352,69 +319,31 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
<span class="font-mono text-xs break-all">{r().tenant}</span>
</Field>
</Show>
<Show when={props.editHref && showPlanActions()}>
<Field label=" ">
<Show
when={props.onUpdatePlan}
fallback={
<Show
when={!isTopTier()}
fallback={<span />}
>
<A
href={props.editHref!}
class="inline-flex items-center rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700 hover:bg-blue-100"
>
Upgrade Plan
</A>
</Show>
}
>
<button
type="button"
class="inline-flex items-center rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700 hover:bg-blue-100"
onClick={() => setPlanModalOpen(true)}
>
Update Plan
</button>
</Show>
</Field>
</Show>
</MembershipSection>
<Modal
open={planModalOpen()}
onClose={() => 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"
>
<div class="p-6 sm:p-8">
<h2 class="text-2xl font-bold text-gray-900 mb-2">Update plan</h2>
<p class="text-sm text-gray-500 mb-8">Choose the plan you want for this relay.</p>
<Show when={showPlanActions()}>
<hr class="border-gray-200" />
<PricingTable
selectable
compactOnMobile
selectedPlan={selectedPlan()}
onSelect={setSelectedPlan}
/>
<Show when={planError()}>
<p class="mt-4 text-sm text-red-600">{planError()}</p>
<DetailSection title="Plan">
<Show
when={props.onUpdatePlan}
fallback={
<Field label="Current plan">
<span class="capitalize text-gray-900">{r().plan}</span>
</Field>
}
>
<div class="lg:col-span-2 space-y-4">
<PricingTable
selectable
compactOnMobile
selectedPlan={plan()}
onSelect={changePlan}
/>
</div>
</Show>
<div class="mt-8">
<button
type="button"
onClick={() => 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"}
</button>
</div>
</div>
</Modal>
</DetailSection>
</Show>
</div>
)
}
+11 -6
View File
@@ -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<Relay, "info_name" | "subdomain" | "info_icon" | "info_description" | "plan">
@@ -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) {
<div class="text-sm text-gray-500 mt-1">
{p.price === 0 ? "Free" : `${p.price.toLocaleString()} sats/mo`}
</div>
<div class="text-xs text-gray-500 mt-2">{p.members} members</div>
<div class="text-xs text-gray-500 mt-2">{p.memberLabel}</div>
</button>
))}
</div>
</div>
{error() && <p class="text-sm text-red-600">{error()}</p>}
<button
type="submit"
disabled={submitting()}
+7 -11
View File
@@ -1,12 +1,8 @@
import { Show, createEffect, createSignal, onCleanup } from "solid-js"
type ToastProps = {
message?: string
duration?: number
onClear?: () => 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 (
<Show when={props.message}>
<Show when={toastMessage()}>
<div
role="alert"
class="fixed bottom-4 right-4 z-50 max-w-md rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-base text-red-700 shadow-lg transition-all duration-400 ease-out"
@@ -67,7 +63,7 @@ export default function Toast(props: ToastProps) {
"translate-y-3 opacity-0 scale-95": !visible(),
}}
>
{props.message}
{toastMessage()}
</div>
</Show>
)