From 034572cb587c40c0d2dcb51c0e458a4f9757a5bc Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 27 Feb 2026 14:40:51 -0800 Subject: [PATCH] Switch to different navigation style --- .fdignore | 1 + frontend/bun.lock | 3 + frontend/package.json | 1 + frontend/src/App.tsx | 37 ++- frontend/src/assets/server.svg | 8 + frontend/src/components/AppShell.tsx | 253 ++++++++++++++++++ frontend/src/components/LoadingState.tsx | 15 ++ frontend/src/components/ResourceState.tsx | 5 +- frontend/src/components/useMinLoading.ts | 41 +++ frontend/src/lib/api.ts | 3 + frontend/src/pages/Account.tsx | 77 +++--- frontend/src/pages/Home.tsx | 4 +- frontend/src/pages/Login.tsx | 2 +- frontend/src/pages/admin/AdminRelayDetail.tsx | 8 +- frontend/src/pages/admin/AdminRelayEdit.tsx | 8 +- frontend/src/pages/admin/AdminRelayList.tsx | 78 ++++++ frontend/src/pages/admin/AdminRelays.tsx | 62 ----- .../src/pages/admin/AdminTenantDetail.tsx | 112 ++++---- frontend/src/pages/admin/AdminTenantList.tsx | 130 +++++++++ frontend/src/pages/admin/AdminTenants.tsx | 103 ------- frontend/src/pages/relays/RelayDetail.tsx | 8 +- frontend/src/pages/relays/RelayEdit.tsx | 8 +- frontend/src/pages/relays/RelayList.tsx | 90 ++++--- frontend/src/pages/relays/RelayNew.tsx | 2 +- 24 files changed, 724 insertions(+), 335 deletions(-) create mode 100644 frontend/src/assets/server.svg create mode 100644 frontend/src/components/AppShell.tsx create mode 100644 frontend/src/components/LoadingState.tsx create mode 100644 frontend/src/components/useMinLoading.ts create mode 100644 frontend/src/pages/admin/AdminRelayList.tsx delete mode 100644 frontend/src/pages/admin/AdminRelays.tsx create mode 100644 frontend/src/pages/admin/AdminTenantList.tsx delete mode 100644 frontend/src/pages/admin/AdminTenants.tsx diff --git a/.fdignore b/.fdignore index 47f39b2..d3cda6f 100644 --- a/.fdignore +++ b/.fdignore @@ -1,3 +1,4 @@ ref target .agents +.playwright-cli diff --git a/frontend/bun.lock b/frontend/bun.lock index 3d14afe..af2f34e 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -17,6 +17,7 @@ "applesauce-relay": "^5.1.0", "applesauce-signers": "^5.1.0", "applesauce-wallet-connect": "^5.0.1", + "fuse.js": "^7.1.0", "preline": "^4.1.1", "qr-scanner": "^1.4.2", "qrcode": "^1.5.4", @@ -348,6 +349,8 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], diff --git a/frontend/package.json b/frontend/package.json index 75ea692..cf1b044 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "applesauce-relay": "^5.1.0", "applesauce-signers": "^5.1.0", "applesauce-wallet-connect": "^5.0.1", + "fuse.js": "^7.1.0", "preline": "^4.1.1", "qr-scanner": "^1.4.2", "qrcode": "^1.5.4", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index be1f655..9020f9a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,7 @@ -import { createEffect, createResource, Show } from "solid-js" +import { createEffect, Show } from "solid-js" import { Router, Route, useLocation, useNavigate } from "@solidjs/router" import type { Component } from "solid-js" -import Navbar from "./components/Navbar" +import AppShell from "./components/AppShell" import Home from "./pages/Home" import Login from "./pages/Login" import RelayList from "./pages/relays/RelayList" @@ -9,16 +9,21 @@ import RelayNew from "./pages/relays/RelayNew" import RelayDetail from "./pages/relays/RelayDetail" import RelayEdit from "./pages/relays/RelayEdit" import Account from "./pages/Account" -import AdminTenants from "./pages/admin/AdminTenants" +import AdminTenantList from "./pages/admin/AdminTenantList" import AdminTenantDetail from "./pages/admin/AdminTenantDetail" -import AdminRelays from "./pages/admin/AdminRelays" +import AdminRelayList from "./pages/admin/AdminRelayList" import AdminRelayDetail from "./pages/admin/AdminRelayDetail" import AdminRelayEdit from "./pages/admin/AdminRelayEdit" import { useActiveAccount } from "./lib/nostr" -import { adminCheck as fetchAdminCheck } from "./lib/api" function Layout(props: { children?: any }) { const location = useLocation() + const account = useActiveAccount() + + const usesAppShell = () => { + const path = location.pathname + return path.startsWith("/relays") || path.startsWith("/account") || path.startsWith("/admin") + } createEffect(() => { // Reinitialize Preline components on route change @@ -28,8 +33,9 @@ function Layout(props: { children?: any }) { return (
- -
{props.children}
+ {props.children}}> + {props.children} +
) } @@ -52,10 +58,6 @@ export default function App() { return () => { const navigate = useNavigate() const account = useActiveAccount() - const [adminCheck] = createResource( - () => account()?.id, - () => fetchAdminCheck(), - ) createEffect(() => { if (!account()) navigate("/login", { replace: true }) @@ -63,14 +65,7 @@ export default function App() { return ( - Checking admin access...}> - You do not have admin access.} - > - - - + ) } @@ -85,9 +80,9 @@ export default function App() { - + - + diff --git a/frontend/src/assets/server.svg b/frontend/src/assets/server.svg new file mode 100644 index 0000000..e5954c3 --- /dev/null +++ b/frontend/src/assets/server.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx new file mode 100644 index 0000000..a65c103 --- /dev/null +++ b/frontend/src/components/AppShell.tsx @@ -0,0 +1,253 @@ +import { A, useLocation } from "@solidjs/router" +import { createEffect, createMemo, createResource, createSignal, For, onCleanup, Show } from "solid-js" +import Fuse from "fuse.js" +import { adminCheck as fetchAdminCheck, listTenantRelays, type Relay } from "../lib/api" +import { eventStore, primeProfiles, useActiveAccount, useProfilePicture } from "../lib/nostr" +import serverIcon from "../assets/server.svg" + +type Profile = { + name?: string + display_name?: string + nip05?: string +} + +function shortenPubkey(pubkey?: string) { + if (!pubkey) return "" + return `${pubkey.slice(0, 8)}…${pubkey.slice(-6)}` +} + +function SearchIcon() { + return ( + + ) +} + +function RelayIcon() { + return +} + +export default function AppShell(props: { children?: any }) { + const location = useLocation() + const account = useActiveAccount() + const picture = useProfilePicture(() => account()?.pubkey) + const [adminCheck] = createResource(() => account()?.id, () => fetchAdminCheck()) + const [tenantRelays] = createResource(() => account()?.id, () => listTenantRelays()) + const [profile, setProfile] = createSignal({}) + const [searchOpen, setSearchOpen] = createSignal(false) + const [searchEntered, setSearchEntered] = createSignal(false) + const [searchQuery, setSearchQuery] = createSignal("") + let searchTransitionTimer: number | undefined + + const isAdmin = createMemo(() => !!adminCheck()?.is_admin) + const username = createMemo(() => profile().name || profile().display_name || shortenPubkey(account()?.pubkey)) + const nip05 = createMemo(() => profile().nip05 || "No NIP-05") + const searchedRelays = createMemo(() => { + const list = tenantRelays() ?? [] + const query = searchQuery().trim() + + if (!query) return list + + const fuse = new Fuse(list, { + keys: ["name", "subdomain"], + threshold: 0.35, + ignoreLocation: true, + }) + + return fuse.search(query).map((result) => result.item) + }) + + createEffect(() => { + const pubkey = account()?.pubkey + + if (!pubkey) { + setProfile({}) + return + } + + const profileSub = eventStore.profile(pubkey).subscribe((metadata) => { + setProfile({ + name: metadata?.name, + display_name: metadata?.display_name, + nip05: metadata?.nip05, + }) + }) + + const primeSub = primeProfiles([pubkey]) + + onCleanup(() => { + profileSub.unsubscribe() + primeSub.unsubscribe() + }) + }) + + const myResources = [{ href: "/relays", label: "My Relays" }] + const adminResources = [ + { href: "/admin/tenants", label: "Tenants" }, + { href: "/admin/relays", label: "Relays" }, + ] + + const navItemClass = (href: string) => { + const active = location.pathname === href || location.pathname.startsWith(`${href}/`) + return active + ? "block rounded-lg bg-white/15 px-3 py-2 text-sm font-medium text-white" + : "block rounded-lg px-3 py-2 text-sm text-white/80 hover:bg-white/10 hover:text-white" + } + + const openSearchModal = () => { + if (searchTransitionTimer) window.clearTimeout(searchTransitionTimer) + setSearchOpen(true) + setSearchEntered(false) + window.requestAnimationFrame(() => setSearchEntered(true)) + } + + const closeSearchModal = () => { + setSearchEntered(false) + searchTransitionTimer = window.setTimeout(() => { + setSearchOpen(false) + setSearchQuery("") + }, 200) + } + + onCleanup(() => { + if (searchTransitionTimer) window.clearTimeout(searchTransitionTimer) + }) + + return ( +
+
+ + +
+
{props.children}
+
+ + + + + + + + ) +} diff --git a/frontend/src/components/LoadingState.tsx b/frontend/src/components/LoadingState.tsx new file mode 100644 index 0000000..14b7b5f --- /dev/null +++ b/frontend/src/components/LoadingState.tsx @@ -0,0 +1,15 @@ +type LoadingStateProps = { + message: string + class?: string + paddingClass?: string +} + +export default function LoadingState(props: LoadingStateProps) { + const paddingClass = () => props.paddingClass ?? "py-20" + return ( +
+
+

{props.message}

+
+ ) +} diff --git a/frontend/src/components/ResourceState.tsx b/frontend/src/components/ResourceState.tsx index a496366..968d8ad 100644 --- a/frontend/src/components/ResourceState.tsx +++ b/frontend/src/components/ResourceState.tsx @@ -1,4 +1,5 @@ import { Show } from "solid-js" +import LoadingState from "./LoadingState" type ResourceStateProps = { loading: boolean @@ -12,10 +13,10 @@ export default function ResourceState(props: ResourceStateProps) { return ( <> -

{props.loadingText}

+
-

{props.errorText}

+

{props.errorText}

) diff --git a/frontend/src/components/useMinLoading.ts b/frontend/src/components/useMinLoading.ts new file mode 100644 index 0000000..518c37a --- /dev/null +++ b/frontend/src/components/useMinLoading.ts @@ -0,0 +1,41 @@ +import { createEffect, createSignal, onCleanup } from "solid-js" + +export default function useMinLoading(loading: () => boolean, minDurationMs = 200) { + const [visible, setVisible] = createSignal(false) + let startTime = 0 + let timeout: number | undefined + + createEffect(() => { + if (timeout) { + window.clearTimeout(timeout) + timeout = undefined + } + + if (loading()) { + startTime = Date.now() + setVisible(true) + return + } + + if (!visible()) return + + const elapsed = Date.now() - startTime + const remaining = Math.max(minDurationMs - elapsed, 0) + + if (remaining === 0) { + setVisible(false) + return + } + + timeout = window.setTimeout(() => { + setVisible(false) + timeout = undefined + }, remaining) + + onCleanup(() => { + if (timeout) window.clearTimeout(timeout) + }) + }) + + return visible +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index e4b2610..97b37b1 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -74,6 +74,9 @@ async function request(path: string, init?: RequestInit): Promise { } catch { // ignored } + if (response.status === 403 && path.startsWith("/admin/") && typeof window !== "undefined") { + window.location.replace("/relays") + } throw new ApiError(message, response.status) } diff --git a/frontend/src/pages/Account.tsx b/frontend/src/pages/Account.tsx index 1c7f503..106616f 100644 --- a/frontend/src/pages/Account.tsx +++ b/frontend/src/pages/Account.tsx @@ -1,7 +1,8 @@ -import { createMemo, createResource, createSignal, For, Show } from "solid-js" +import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js" import { getTenant, listTenantInvoices, updateTenantBilling } from "../lib/api" import PageContainer from "../components/PageContainer" -import ResourceState from "../components/ResourceState" +import LoadingState from "../components/LoadingState" +import useMinLoading from "../components/useMinLoading" export default function Account() { const [tenant, { refetch: refetchTenant }] = createResource(getTenant) @@ -9,15 +10,24 @@ export default function Account() { const [nwcUrl, setNwcUrl] = createSignal("") const [saving, setSaving] = createSignal(false) const [error, setError] = createSignal("") + const invoicesLoading = useMinLoading(() => invoices.loading) - const recurringEnabled = createMemo(() => !!tenant()?.tenant_nwc_url?.trim()) + const hasBillingChanges = createMemo(() => { + const current = tenant()?.tenant_nwc_url?.trim() ?? "" + const next = nwcUrl().trim() + return current !== next + }) + + createEffect(() => { + setNwcUrl(tenant()?.tenant_nwc_url ?? "") + }) async function saveBilling() { setError("") setSaving(true) try { - await updateTenantBilling(nwcUrl().trim()) - setNwcUrl("") + const next = nwcUrl().trim() + await updateTenantBilling(next) await refetchTenant() } catch (e) { setError(e instanceof Error ? e.message : "Failed to update billing") @@ -28,7 +38,7 @@ export default function Account() { return ( -

My Account

+

My Account

@@ -42,12 +52,6 @@ export default function Account() { )}
-
@@ -66,22 +70,11 @@ export default function Account() { -

{error()}

@@ -90,24 +83,26 @@ export default function Account() {

Invoice History

- -

Loading invoices...

+ + - 0} fallback={

No invoices yet.

}> -
    - - {(invoice) => ( -
  • -
    - {invoice.amount.toLocaleString()} sats - {invoice.status} -
    -

    {new Date(invoice.created_at).toLocaleString()}

    -

    {invoice.invoice}

    -
  • - )} -
    -
+ + 0} fallback={

No invoices yet.

}> +
    + + {(invoice) => ( +
  • +
    + {invoice.amount.toLocaleString()} sats + {invoice.status} +
    +

    {new Date(invoice.created_at).toLocaleString()}

    +

    {invoice.invoice}

    +
  • + )} +
    +
+
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 6e0d73e..eda24ac 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -5,7 +5,7 @@ export default function Home() {
{/* Hero */}
-

+

Host Your Own Nostr Community Relay

@@ -22,7 +22,7 @@ export default function Home() { {/* Pricing */}

-

Pricing

+

Pricing

Free

diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 0288168..5e5b5a2 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -206,7 +206,7 @@ export default function Login() { Secure Nostr Login
-

Welcome back

+

Welcome back

Connect your Nostr account to manage relay hosting, billing, and access in one place.

diff --git a/frontend/src/pages/admin/AdminRelayDetail.tsx b/frontend/src/pages/admin/AdminRelayDetail.tsx index 1df0432..9089ef9 100644 --- a/frontend/src/pages/admin/AdminRelayDetail.tsx +++ b/frontend/src/pages/admin/AdminRelayDetail.tsx @@ -4,6 +4,7 @@ import { adminDeactivateRelay, adminGetRelay } from "../../lib/api" import BackLink from "../../components/BackLink" import PageContainer from "../../components/PageContainer" import ResourceState from "../../components/ResourceState" +import useMinLoading from "../../components/useMinLoading" export default function AdminRelayDetail() { const params = useParams() @@ -11,6 +12,7 @@ export default function AdminRelayDetail() { const [relay, { refetch }] = createResource(relayId, adminGetRelay) const [busy, setBusy] = createSignal(false) const [error, setError] = createSignal("") + const loading = useMinLoading(() => relay.loading) async function handleDeactivate() { if (busy()) return @@ -31,17 +33,17 @@ export default function AdminRelayDetail() { - + {(r) => (
-

{r().name}

+

{r().name}

{r().subdomain}.spaces.coracle.social

Tenant: {r().tenant}

Plan: {r().plan}

diff --git a/frontend/src/pages/admin/AdminRelayEdit.tsx b/frontend/src/pages/admin/AdminRelayEdit.tsx index af5f41f..2f36acf 100644 --- a/frontend/src/pages/admin/AdminRelayEdit.tsx +++ b/frontend/src/pages/admin/AdminRelayEdit.tsx @@ -7,12 +7,14 @@ import { slugify } from "../../lib/slugify" import BackLink from "../../components/BackLink" import PageContainer from "../../components/PageContainer" import ResourceState from "../../components/ResourceState" +import useMinLoading from "../../components/useMinLoading" export default function AdminRelayEdit() { const navigate = useNavigate() const params = useParams() const relayId = () => params.id ?? "" const [relay] = createResource(relayId, adminGetRelay) + const loading = useMinLoading(() => relay.loading) const [name, setName] = createSignal("") const [subdomain, setSubdomain] = createSignal("") @@ -55,16 +57,16 @@ export default function AdminRelayEdit() { return ( -

Edit Relay (Admin)

+

Edit Relay (Admin)

- + relays.loading) + + const filtered = createMemo(() => { + const list = relays() ?? [] + const q = query().trim() + + if (!q) return list + + return new Fuse(list, { + keys: ["name", "subdomain", "tenant"], + threshold: 0.35, + ignoreLocation: true, + }).search(q).map((result) => result.item) + }) + + return ( + +
+

Relays

+
+
+ + + + setQuery(e.currentTarget.value)} + placeholder="Search relays..." + class="w-full border border-gray-300 rounded-lg py-2 pl-10 pr-3 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + + + 0} fallback={

No relays found.

}> + +
+
+
+ ) +} diff --git a/frontend/src/pages/admin/AdminRelays.tsx b/frontend/src/pages/admin/AdminRelays.tsx deleted file mode 100644 index 4c6c209..0000000 --- a/frontend/src/pages/admin/AdminRelays.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { A } from "@solidjs/router" -import { createMemo, createResource, createSignal, For, Show } from "solid-js" -import { adminListRelays } from "../../lib/api" -import PageContainer from "../../components/PageContainer" -import ResourceState from "../../components/ResourceState" - -export default function AdminRelays() { - const [query, setQuery] = createSignal("") - const [relays] = createResource(adminListRelays) - - const filtered = createMemo(() => { - const q = query().trim().toLowerCase() - return (relays() ?? []).filter((relay) => { - if (!q) return true - return ( - relay.name.toLowerCase().includes(q) - || relay.subdomain.toLowerCase().includes(q) - || relay.tenant.toLowerCase().includes(q) - ) - }) - }) - - return ( - -

All Relays

- setQuery(e.currentTarget.value)} - placeholder="Search relays..." - class="w-full border border-gray-300 rounded-lg px-3 py-2 mb-6 focus:outline-none focus:ring-2 focus:ring-blue-500" - /> - - - 0} fallback={

No relays found.

}> - -
-
- ) -} diff --git a/frontend/src/pages/admin/AdminTenantDetail.tsx b/frontend/src/pages/admin/AdminTenantDetail.tsx index 13c77ad..5f22023 100644 --- a/frontend/src/pages/admin/AdminTenantDetail.tsx +++ b/frontend/src/pages/admin/AdminTenantDetail.tsx @@ -4,6 +4,7 @@ import { adminGetTenant, adminUpdateTenantStatus } from "../../lib/api" import BackLink from "../../components/BackLink" import PageContainer from "../../components/PageContainer" import ResourceState from "../../components/ResourceState" +import useMinLoading from "../../components/useMinLoading" export default function AdminTenantDetail() { const params = useParams() @@ -11,6 +12,7 @@ export default function AdminTenantDetail() { const [detail, { refetch }] = createResource(tenantId, adminGetTenant) const [busy, setBusy] = createSignal(false) const [error, setError] = createSignal("") + const loading = useMinLoading(() => detail.loading) async function setStatus(status: string) { if (busy()) return @@ -29,72 +31,74 @@ export default function AdminTenantDetail() { return ( -

Tenant {params.id}

+

Tenant {params.id}

-
-
-

Status

- - {(d) => ( -
-

- Current: {d().tenant.status} -

-
- - + +
+
+

Status

+ + {(d) => ( +
+

+ Current: {d().tenant.status} +

+
+ + +
-
- )} -
-
+ )} + + -
-

Relays

- 0} fallback={

No relays.

}> - + + + )} + + +
+
+ +

{error()}

- - -

{error()}

-
-
+
+
) } diff --git a/frontend/src/pages/admin/AdminTenantList.tsx b/frontend/src/pages/admin/AdminTenantList.tsx new file mode 100644 index 0000000..b64298d --- /dev/null +++ b/frontend/src/pages/admin/AdminTenantList.tsx @@ -0,0 +1,130 @@ +import { A } from "@solidjs/router" +import Fuse from "fuse.js" +import { createEffect, createMemo, createResource, createSignal, For, onCleanup, Show } from "solid-js" +import { getProfilePicture } from "applesauce-core/helpers/profile" +import { adminListTenants } from "../../lib/api" +import { eventStore, primeProfiles } from "../../lib/nostr" +import PageContainer from "../../components/PageContainer" +import ResourceState from "../../components/ResourceState" +import useMinLoading from "../../components/useMinLoading" + +function shortenPubkey(pubkey: string) { + if (pubkey.length <= 16) return pubkey + return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}` +} + +export default function AdminTenantList() { + const [query, setQuery] = createSignal("") + const [tenants] = createResource(adminListTenants) + const [profiles, setProfiles] = createSignal>({}) + const loading = useMinLoading(() => tenants.loading) + + const filtered = createMemo(() => { + const list = (tenants() ?? []).map((tenant) => { + const profile = profiles()[tenant.pubkey] + return { + ...tenant, + profileName: profile?.name, + profileAbout: profile?.about, + profileNip05: profile?.nip05, + } + }) + const q = query().trim() + + if (!q) return list + + return new Fuse(list, { + keys: ["pubkey", "status", "profileName", "profileAbout", "profileNip05"], + threshold: 0.35, + ignoreLocation: true, + }).search(q).map((result) => result.item) + }) + + createEffect(() => { + const list = tenants() ?? [] + if (!list.length) return + + const pubkeys = list.map(t => t.pubkey) + const reqSub = primeProfiles(pubkeys) + const profileSubs = pubkeys.map((pubkey) => + eventStore.profile(pubkey).subscribe((profile) => { + setProfiles(prev => ({ + ...prev, + [pubkey]: { + name: profile?.name || profile?.display_name, + about: profile?.about, + nip05: profile?.nip05, + picture: getProfilePicture(profile), + }, + })) + }), + ) + + onCleanup(() => { + reqSub.unsubscribe() + for (const sub of profileSubs) sub.unsubscribe() + }) + }) + + return ( + +

Tenants

+
+ + + + setQuery(e.currentTarget.value)} + placeholder="Search tenants..." + class="w-full border border-gray-300 rounded-lg py-2 pl-10 pr-3 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + + + 0} fallback={

No tenants found.

}> +
+ + + ) + }} + + + + + + ) +} diff --git a/frontend/src/pages/admin/AdminTenants.tsx b/frontend/src/pages/admin/AdminTenants.tsx deleted file mode 100644 index 4ae5b77..0000000 --- a/frontend/src/pages/admin/AdminTenants.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { A } from "@solidjs/router" -import { createEffect, createMemo, createResource, createSignal, For, onCleanup, Show } from "solid-js" -import { getProfilePicture } from "applesauce-core/helpers/profile" -import { adminListTenants } from "../../lib/api" -import { eventStore, primeProfiles } from "../../lib/nostr" -import PageContainer from "../../components/PageContainer" -import ResourceState from "../../components/ResourceState" - -function shortenPubkey(pubkey: string) { - if (pubkey.length <= 16) return pubkey - return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}` -} - -export default function AdminTenants() { - const [query, setQuery] = createSignal("") - const [tenants] = createResource(adminListTenants) - const [profiles, setProfiles] = createSignal>({}) - - const filtered = createMemo(() => { - const q = query().trim().toLowerCase() - return (tenants() ?? []).filter((tenant) => { - if (!q) return true - return tenant.pubkey.toLowerCase().includes(q) || tenant.status.toLowerCase().includes(q) - }) - }) - - createEffect(() => { - const list = tenants() ?? [] - if (!list.length) return - - const pubkeys = list.map(t => t.pubkey) - const reqSub = primeProfiles(pubkeys) - const profileSubs = pubkeys.map((pubkey) => - eventStore.profile(pubkey).subscribe((profile) => { - setProfiles(prev => ({ - ...prev, - [pubkey]: { - name: profile?.name || profile?.display_name, - about: profile?.about, - picture: getProfilePicture(profile), - }, - })) - }), - ) - - onCleanup(() => { - reqSub.unsubscribe() - for (const sub of profileSubs) sub.unsubscribe() - }) - }) - - return ( - -

Tenants

- setQuery(e.currentTarget.value)} - placeholder="Search tenants..." - class="w-full border border-gray-300 rounded-lg px-3 py-2 mb-6 focus:outline-none focus:ring-2 focus:ring-blue-500" - /> - - - 0} fallback={

No tenants found.

}> -
- - - ) - }} - - - - - ) -} diff --git a/frontend/src/pages/relays/RelayDetail.tsx b/frontend/src/pages/relays/RelayDetail.tsx index 53823b1..0178e65 100644 --- a/frontend/src/pages/relays/RelayDetail.tsx +++ b/frontend/src/pages/relays/RelayDetail.tsx @@ -4,6 +4,7 @@ import { deactivateTenantRelay, getTenantRelay } from "../../lib/api" import BackLink from "../../components/BackLink" import PageContainer from "../../components/PageContainer" import ResourceState from "../../components/ResourceState" +import useMinLoading from "../../components/useMinLoading" export default function RelayDetail() { const params = useParams() @@ -11,6 +12,7 @@ export default function RelayDetail() { const [relay, { refetch }] = createResource(relayId, getTenantRelay) const [busy, setBusy] = createSignal(false) const [error, setError] = createSignal("") + const loading = useMinLoading(() => relay.loading) async function handleDeactivate() { if (busy()) return @@ -31,17 +33,17 @@ export default function RelayDetail() { - + {(loadedRelay) => (
-

{loadedRelay().name}

+

{loadedRelay().name}

https://{loadedRelay().subdomain}.spaces.coracle.social

{loadedRelay().description}

diff --git a/frontend/src/pages/relays/RelayEdit.tsx b/frontend/src/pages/relays/RelayEdit.tsx index 847f2bc..b9b696c 100644 --- a/frontend/src/pages/relays/RelayEdit.tsx +++ b/frontend/src/pages/relays/RelayEdit.tsx @@ -7,12 +7,14 @@ import { slugify } from "../../lib/slugify" import BackLink from "../../components/BackLink" import PageContainer from "../../components/PageContainer" import ResourceState from "../../components/ResourceState" +import useMinLoading from "../../components/useMinLoading" export default function RelayEdit() { const navigate = useNavigate() const params = useParams() const relayId = () => params.id ?? "" const [relay] = createResource(relayId, getTenantRelay) + const loading = useMinLoading(() => relay.loading) const [name, setName] = createSignal("") const [subdomain, setSubdomain] = createSignal("") @@ -55,16 +57,16 @@ export default function RelayEdit() { return ( -

Edit Relay

+

Edit Relay

- + relays.loading) const filtered = createMemo(() => { - const q = query().trim().toLowerCase() - return (relays() ?? []).filter((relay) => { - const matchesQuery = - !q || - relay.name.toLowerCase().includes(q) || - relay.subdomain.toLowerCase().includes(q) + const list = relays() ?? [] + const q = query().trim() + const searched = q + ? new Fuse(list, { + keys: ["name", "subdomain"], + threshold: 0.35, + ignoreLocation: true, + }).search(q).map((result) => result.item) + : list + + return searched.filter((relay) => { const matchesStatus = status() === "all" || relay.status === status() - return matchesQuery && matchesStatus + return matchesStatus }) }) return (