More refactoring and bugfixing
This commit is contained in:
@@ -99,8 +99,8 @@ export default function AppShell(props: { children?: any }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="min-h-screen bg-gray-50 md:grid md:grid-cols-[260px_minmax(0,1fr)]">
|
<div class="min-h-screen bg-gray-50">
|
||||||
<aside class="hidden bg-slate-900 text-white md:flex md:min-h-screen md:flex-col">
|
<aside class="hidden md:flex fixed inset-y-0 left-0 w-[260px] bg-slate-900 text-white flex-col z-10">
|
||||||
<div class="flex-1 px-4 py-6">
|
<div class="flex-1 px-4 py-6">
|
||||||
<h2 class="px-3 text-xs font-semibold uppercase tracking-wider text-white/60">Resources</h2>
|
<h2 class="px-3 text-xs font-semibold uppercase tracking-wider text-white/60">Resources</h2>
|
||||||
<ul class="mt-2 space-y-1">
|
<ul class="mt-2 space-y-1">
|
||||||
@@ -137,7 +137,7 @@ export default function AppShell(props: { children?: any }) {
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="min-h-screen pb-20 md:pb-0">
|
<div class="md:pl-[260px] min-h-screen pb-20 md:pb-0">
|
||||||
<main>{props.children}</main>
|
<main>{props.children}</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { JSX } from "solid-js"
|
||||||
|
|
||||||
|
export default function Field(props: { label: string; children: JSX.Element }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<dt class="text-xs font-medium text-gray-500 uppercase tracking-wide">{props.label}</dt>
|
||||||
|
<dd class="mt-0.5 text-sm text-gray-900">{props.children}</dd>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { Show } from "solid-js"
|
|
||||||
import { A } from "@solidjs/router"
|
|
||||||
import { useProfilePicture } from "@/lib/hooks"
|
|
||||||
import { PLATFORM_NAME, account } from "@/lib/state"
|
|
||||||
|
|
||||||
export default function Navbar() {
|
|
||||||
const picture = useProfilePicture(() => account()?.pubkey)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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" />
|
|
||||||
<span class="font-bold text-gray-900 text-lg">{PLATFORM_NAME}</span>
|
|
||||||
</A>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<Show
|
|
||||||
when={account()}
|
|
||||||
fallback={
|
|
||||||
<A
|
|
||||||
href="/login"
|
|
||||||
class="text-sm py-1.5 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
Log In
|
|
||||||
</A>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<A href="/account">
|
|
||||||
<Show
|
|
||||||
when={picture()}
|
|
||||||
fallback={
|
|
||||||
<div class="h-8 w-8 rounded-full bg-gray-200 flex items-center justify-center">
|
|
||||||
<svg class="w-4 h-4 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={picture()}
|
|
||||||
alt="Profile"
|
|
||||||
class="h-8 w-8 rounded-full object-cover"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</A>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { A } from "@solidjs/router"
|
|
||||||
import { For } from "solid-js"
|
import { For } from "solid-js"
|
||||||
import { PLANS, type PlanId } from "@/lib/api"
|
import { PLANS, type PlanId } from "@/lib/api"
|
||||||
|
|
||||||
@@ -24,13 +23,12 @@ type PricingTableProps = {
|
|||||||
selectable?: boolean
|
selectable?: boolean
|
||||||
selectedPlan?: PlanId
|
selectedPlan?: PlanId
|
||||||
onSelect?: (plan: PlanId) => void
|
onSelect?: (plan: PlanId) => void
|
||||||
ctaHref?: string
|
onCta?: (plan: PlanId) => void
|
||||||
compactOnMobile?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PricingTable(props: PricingTableProps) {
|
export default function PricingTable(props: PricingTableProps) {
|
||||||
return (
|
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"}`}>
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
|
||||||
<For each={PLANS}>
|
<For each={PLANS}>
|
||||||
{(plan) => {
|
{(plan) => {
|
||||||
const isPopular = plan.id === "basic"
|
const isPopular = plan.id === "basic"
|
||||||
@@ -43,13 +41,13 @@ export default function PricingTable(props: PricingTableProps) {
|
|||||||
POPULAR
|
POPULAR
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<h3 class={`font-bold text-gray-900 mb-1 ${props.compactOnMobile ? "text-sm sm:text-lg" : "text-lg"}`}>{plan.label}</h3>
|
<h3 class="text-lg font-bold text-gray-900 mb-1">{plan.label}</h3>
|
||||||
<p class={`text-gray-400 mb-6 ${props.compactOnMobile ? "hidden sm:block text-sm" : "text-sm"}`}>{plan.subtitle}</p>
|
<p class="text-sm text-gray-400 mb-6">{plan.subtitle}</p>
|
||||||
<div class={props.compactOnMobile ? "mb-3 sm:mb-8" : "mb-8"}>
|
<div class="mb-8">
|
||||||
<span class={`font-extrabold text-gray-900 ${props.compactOnMobile ? "text-xl sm:text-4xl" : "text-4xl"}`}>{plan.priceLabel}</span>
|
<span class="text-4xl font-extrabold text-gray-900">{plan.priceLabel}</span>
|
||||||
<span class={`text-gray-400 ml-1 ${props.compactOnMobile ? "text-[10px] sm:text-sm" : "text-sm"}`}>sats / mo</span>
|
<span class="text-sm text-gray-400 ml-1">sats / mo</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class={`mb-8 text-sm text-gray-600 ${props.compactOnMobile ? "hidden sm:block space-y-3" : "space-y-3"}`}>
|
<ul class="mb-8 text-sm text-gray-600 space-y-3">
|
||||||
<li class="flex items-start gap-2"><CheckIcon />{plan.memberLabel}</li>
|
<li class="flex items-start gap-2"><CheckIcon />{plan.memberLabel}</li>
|
||||||
<li class={`flex items-start gap-2 ${plan.blossom ? "" : "text-gray-300"}`}>
|
<li class={`flex items-start gap-2 ${plan.blossom ? "" : "text-gray-300"}`}>
|
||||||
{plan.blossom ? <CheckIcon /> : <XIcon />}
|
{plan.blossom ? <CheckIcon /> : <XIcon />}
|
||||||
@@ -60,26 +58,14 @@ export default function PricingTable(props: PricingTableProps) {
|
|||||||
LiveKit video
|
LiveKit video
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{props.compactOnMobile && props.selectable && (
|
|
||||||
<div class="sm:hidden mb-3 text-[10px] text-gray-500 space-y-1">
|
|
||||||
<div>{plan.memberLabel.replace(" members", "")}</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<span class={plan.blossom ? "text-blue-600" : "text-gray-300"}>{plan.blossom ? "✓" : "✕"}</span>
|
|
||||||
Blossom
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<span class={plan.livekit ? "text-blue-600" : "text-gray-300"}>{plan.livekit ? "✓" : "✕"}</span>
|
|
||||||
LiveKit
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!props.selectable && (
|
{!props.selectable && (
|
||||||
<A
|
<button
|
||||||
href={props.ctaHref ?? "/relays/new"}
|
type="button"
|
||||||
class={`block text-center rounded-xl font-semibold transition-colors ${props.compactOnMobile ? "py-1.5 px-2 text-[11px] sm:py-2.5 sm:px-4 sm:text-sm" : "py-2.5 px-4 text-sm"} ${isPopular ? "bg-blue-600 text-white hover:bg-blue-700" : "border border-gray-200 text-gray-700 hover:bg-gray-50"}`}
|
onClick={() => props.onCta?.(plan.id)}
|
||||||
|
class={`w-full text-center py-2.5 px-4 text-sm rounded-xl font-semibold transition-colors ${isPopular ? "bg-blue-600 text-white hover:bg-blue-700" : "border border-gray-200 text-gray-700 hover:bg-gray-50"}`}
|
||||||
>
|
>
|
||||||
Get started
|
Get started
|
||||||
</A>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -89,7 +75,7 @@ export default function PricingTable(props: PricingTableProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => props.onSelect?.(plan.id)}
|
onClick={() => props.onSelect?.(plan.id)}
|
||||||
class={`relative w-full bg-white rounded-2xl text-left transition-all ${props.compactOnMobile ? "p-3 sm:p-8" : "p-8"} ${isSelected() ? "border-2 border-blue-600 shadow-lg shadow-blue-100" : "border border-gray-200 hover:border-gray-300"}`}
|
class={`relative w-full bg-white rounded-2xl p-8 text-left transition-all border-2 ${isSelected() ? "border-blue-600 shadow-lg shadow-blue-100" : "border-gray-200 hover:border-gray-300"}`}
|
||||||
>
|
>
|
||||||
{card}
|
{card}
|
||||||
</button>
|
</button>
|
||||||
@@ -97,7 +83,7 @@ export default function PricingTable(props: PricingTableProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`relative bg-white rounded-2xl ${props.compactOnMobile ? "p-3 sm:p-8" : "p-8"} ${isPopular ? "border-2 border-blue-600 shadow-lg shadow-blue-100" : "border border-gray-200"}`}>
|
<div class={`relative bg-white rounded-2xl p-8 ${isPopular ? "border-2 border-blue-600 shadow-lg shadow-blue-100" : "border border-gray-200"}`}>
|
||||||
{card}
|
{card}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -298,7 +298,6 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
|
|||||||
<div class="lg:col-span-2 space-y-4">
|
<div class="lg:col-span-2 space-y-4">
|
||||||
<PricingTable
|
<PricingTable
|
||||||
selectable
|
selectable
|
||||||
compactOnMobile
|
|
||||||
selectedPlan={plan()}
|
selectedPlan={plan()}
|
||||||
onSelect={changePlan}
|
onSelect={changePlan}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export default function ToggleButton(props: {
|
||||||
|
enabled: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
onToggle?: () => void
|
||||||
|
onLabel?: string
|
||||||
|
offLabel?: string
|
||||||
|
}) {
|
||||||
|
const label = () => (props.enabled ? (props.onLabel ?? "Enabled") : (props.offLabel ?? "Disabled"))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="inline-flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={props.enabled}
|
||||||
|
aria-label={label()}
|
||||||
|
class={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${props.enabled ? "bg-blue-600" : "bg-gray-300"} disabled:opacity-50`}
|
||||||
|
onClick={props.onToggle}
|
||||||
|
disabled={props.disabled}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${props.enabled ? "translate-x-5" : "translate-x-0.5"}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<span class={`text-xs font-medium ${props.enabled ? "text-blue-700" : "text-gray-500"}`}>{label()}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { JSX } from "solid-js"
|
||||||
|
|
||||||
|
export default function ToggleField(props: { label: string; children: JSX.Element }) {
|
||||||
|
return (
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<dt class="text-xs font-medium text-gray-500 uppercase tracking-wide">{props.label}</dt>
|
||||||
|
<dd class="text-sm text-gray-900">{props.children}</dd>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+34
-20
@@ -1,5 +1,5 @@
|
|||||||
import { A, useNavigate } from "@solidjs/router"
|
import { A, useNavigate } from "@solidjs/router"
|
||||||
import { createSignal } from "solid-js"
|
import { createSignal, Show } from "solid-js"
|
||||||
import CheckIcon from "@/components/CheckIcon"
|
import CheckIcon from "@/components/CheckIcon"
|
||||||
import ExternalLinkIcon from "@/components/ExternalLinkIcon"
|
import ExternalLinkIcon from "@/components/ExternalLinkIcon"
|
||||||
import PricingTable from "@/components/PricingTable"
|
import PricingTable from "@/components/PricingTable"
|
||||||
@@ -14,6 +14,12 @@ export default function Home() {
|
|||||||
const [showRelayModal, setShowRelayModal] = createSignal(false)
|
const [showRelayModal, setShowRelayModal] = createSignal(false)
|
||||||
const [showLoginModal, setShowLoginModal] = createSignal(false)
|
const [showLoginModal, setShowLoginModal] = createSignal(false)
|
||||||
const [draftRelay, setDraftRelay] = createSignal<RelayFormValues>()
|
const [draftRelay, setDraftRelay] = createSignal<RelayFormValues>()
|
||||||
|
const [initialPlan, setInitialPlan] = createSignal<RelayFormValues["plan"]>("free")
|
||||||
|
|
||||||
|
function openRelayModal(plan: RelayFormValues["plan"] = "free") {
|
||||||
|
setInitialPlan(plan)
|
||||||
|
setShowRelayModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
async function onRelayFormSubmit(values: RelayFormValues) {
|
async function onRelayFormSubmit(values: RelayFormValues) {
|
||||||
if (account()) {
|
if (account()) {
|
||||||
@@ -47,18 +53,25 @@ export default function Home() {
|
|||||||
<img src="/caravel.png" alt="Caravel" class="w-7 h-7 rounded" />
|
<img src="/caravel.png" alt="Caravel" class="w-7 h-7 rounded" />
|
||||||
Caravel
|
Caravel
|
||||||
</div>
|
</div>
|
||||||
<A
|
<Show
|
||||||
href={account() ? "/relays" : "#"}
|
when={account()}
|
||||||
onClick={(e) => {
|
fallback={
|
||||||
if (!account()) {
|
<button
|
||||||
e.preventDefault()
|
type="button"
|
||||||
setShowLoginModal(true)
|
onClick={() => setShowLoginModal(true)}
|
||||||
}
|
class="text-sm font-medium py-1.5 px-4 border border-gray-200 rounded-lg text-gray-600 hover:bg-gray-50 transition-colors"
|
||||||
}}
|
>
|
||||||
class="text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
|
Sign in
|
||||||
|
</button>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{account() ? "Go to dashboard" : "Sign in"}
|
<A
|
||||||
</A>
|
href="/relays"
|
||||||
|
class="text-sm font-medium py-1.5 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Go to dashboard
|
||||||
|
</A>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -87,10 +100,10 @@ export default function Home() {
|
|||||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowRelayModal(true)}
|
onClick={() => openRelayModal()}
|
||||||
class="inline-flex items-center gap-2 py-3 px-8 bg-blue-600 text-white font-semibold rounded-xl hover:bg-blue-700 active:scale-95 transition-all shadow-lg shadow-blue-200"
|
class="inline-flex items-center gap-2 py-3 px-8 bg-blue-600 text-white font-semibold rounded-xl hover:bg-blue-700 active:scale-95 transition-all shadow-lg shadow-blue-200"
|
||||||
>
|
>
|
||||||
Get started free
|
Get started free
|
||||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7" /></svg>
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7" /></svg>
|
||||||
</button>
|
</button>
|
||||||
<A
|
<A
|
||||||
@@ -291,7 +304,7 @@ export default function Home() {
|
|||||||
Pay in sats. Upgrade or cancel any time.
|
Pay in sats. Upgrade or cancel any time.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<PricingTable ctaHref="#" />
|
<PricingTable onCta={openRelayModal} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -305,7 +318,7 @@ export default function Home() {
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowRelayModal(true)}
|
onClick={() => openRelayModal()}
|
||||||
class="inline-flex items-center gap-2 py-3 px-10 bg-blue-600 text-white font-semibold rounded-xl hover:bg-blue-700 active:scale-95 transition-all shadow-lg shadow-blue-200 text-lg"
|
class="inline-flex items-center gap-2 py-3 px-10 bg-blue-600 text-white font-semibold rounded-xl hover:bg-blue-700 active:scale-95 transition-all shadow-lg shadow-blue-200 text-lg"
|
||||||
>
|
>
|
||||||
Create your relay
|
Create your relay
|
||||||
@@ -352,6 +365,7 @@ export default function Home() {
|
|||||||
|
|
||||||
<RelayForm
|
<RelayForm
|
||||||
syncSubdomainWithName
|
syncSubdomainWithName
|
||||||
|
initialValues={{ plan: initialPlan() }}
|
||||||
onSubmit={onRelayFormSubmit}
|
onSubmit={onRelayFormSubmit}
|
||||||
submitLabel="Continue"
|
submitLabel="Continue"
|
||||||
submittingLabel="Creating..."
|
submittingLabel="Creating..."
|
||||||
@@ -361,8 +375,8 @@ export default function Home() {
|
|||||||
<Modal
|
<Modal
|
||||||
open={showLoginModal()}
|
open={showLoginModal()}
|
||||||
onClose={() => setShowLoginModal(false)}
|
onClose={() => setShowLoginModal(false)}
|
||||||
wrapperClass="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
wrapperClass="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||||
panelClass="w-full max-w-4xl rounded-2xl"
|
panelClass="w-full max-w-3xl mx-4 rounded-2xl"
|
||||||
>
|
>
|
||||||
<Login
|
<Login
|
||||||
inModal
|
inModal
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ export default function Login(props: LoginPageProps = {}) {
|
|||||||
Secure Nostr Login
|
Secure Nostr Login
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-semibold text-gray-900 py-2">Welcome back</h1>
|
<h1 class="text-2xl font-semibold text-gray-900 py-2">Welcome!</h1>
|
||||||
<p class="mt-3 text-sm leading-6 text-gray-600">
|
<p class="mt-3 text-sm leading-6 text-gray-600">
|
||||||
Connect your Nostr account to manage relay hosting, billing, and access in one place.
|
Connect your Nostr account to manage relay hosting, billing, and access in one place.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user