Add frontend spec
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# Components
|
||||
|
||||
This file defines the spec components used across `frontend/spec/pages/*.md`.
|
||||
|
||||
- **page**: Root container for a route-level screen. Carries route metadata like `path`, auth requirements, and layout shell.
|
||||
- **nav**: Top navigation area for primary brand and session-aware links.
|
||||
- **header**: Top content block for a page or section, usually containing a title.
|
||||
- **section**: Logical grouping of related content within a page.
|
||||
- **hero**: Prominent introductory content block for a page.
|
||||
- **heading**: Section heading wrapper that pairs a title with optional supporting copy.
|
||||
- **title**: Primary label text for a page, section, card, or control.
|
||||
- **description**: Supporting explanatory text under a title or heading.
|
||||
- **text**: Generic body text when no more specific semantic component applies.
|
||||
- **help_text**: Small informational guidance text, usually near forms or auth flows.
|
||||
|
||||
- **brand**: Product identity block (name/logo).
|
||||
- **badge**: Small status/highlight label.
|
||||
- **status**: Current state indicator for an entity (for example relay status).
|
||||
- **meta**: Secondary metadata text (IDs, pubkeys, auxiliary attributes).
|
||||
- **copyright**: Footer legal ownership line.
|
||||
|
||||
- **link**: Navigational text/action leading to another route or URL.
|
||||
- **links**: Group of related link components.
|
||||
- **back_link**: Explicit “go back” navigation link to a parent/list view.
|
||||
- **back_button**: Button-style backward navigation within a multi-step screen.
|
||||
- **button**: Generic clickable action control.
|
||||
- **submit_button**: Primary form submission control with optional loading label.
|
||||
|
||||
- **actions**: Container for action controls associated with a section/card.
|
||||
- **controls**: Container for filters/search/sort controls.
|
||||
- **tabs**: Container for tabbed navigation controls.
|
||||
- **tab**: A selectable tab option within `tabs`.
|
||||
- **screen**: Named sub-view/state within a page (e.g., a login subflow).
|
||||
- **modal**: Overlay dialog for focused temporary interaction.
|
||||
|
||||
- **form**: Form container for user input and submission behavior.
|
||||
- **field**: Structured form field abstraction (label + input type + validation intent).
|
||||
- **input**: Single input control for user-entered values.
|
||||
- **select**: Dropdown control for choosing one option from many.
|
||||
- **option**: A selectable value within `select` or plan/method groups.
|
||||
- **copyable_input**: Read-only input-like field designed for quick copy actions.
|
||||
- **tooltip_error**: Inline or anchored validation error shown near a specific field.
|
||||
- **error_message**: Message shown when an action fails.
|
||||
- **error_state**: Full-state error presentation for failed data loading.
|
||||
- **loading_state**: Full-state loading presentation while data is being fetched.
|
||||
|
||||
- **list**: Repeating collection container.
|
||||
- **item**: Single element within a repeating collection.
|
||||
- **grid**: Multi-column layout container for repeated items/cards.
|
||||
- **card**: Bounded content block used in feature grids and summaries.
|
||||
- **product**: Specialized card describing an external ecosystem product/integration.
|
||||
- **relay_detail_card**: Composite card summarizing relay properties and management actions.
|
||||
|
||||
- **avatar**: User/profile image representation.
|
||||
- **bullets**: Container for short point-form highlights.
|
||||
- **toggle**: On/off control for a boolean feature or policy.
|
||||
- **toggles**: Group of related boolean toggle controls.
|
||||
- **plan_selector**: Control used to switch between available relay plans.
|
||||
- **pricing_table**: Plan comparison and pricing presentation block.
|
||||
- **member_count**: Display of current member usage for a relay.
|
||||
- **members**: Plan capacity descriptor (member limits).
|
||||
- **price**: Plan pricing descriptor.
|
||||
- **tenant**: Tenant identity/value display in admin-oriented contexts.
|
||||
- **created_at**: Human-readable creation timestamp display.
|
||||
- **bolt11**: Lightning invoice string display.
|
||||
- **qr_code**: Visual QR representation of a connection URI.
|
||||
- **camera_preview**: Live camera viewport used for scanning QR codes.
|
||||
|
||||
## Control directives used in specs
|
||||
|
||||
These appear in the Pug-like spec language but are flow directives rather than UI components:
|
||||
|
||||
- **if / else**: Conditional branch based on a boolean condition.
|
||||
- **when**: Conditional rendering keyed off a selected tab/state/value.
|
||||
@@ -0,0 +1,11 @@
|
||||
The frontend is a marketing site combined with a tenant and admin dashboard, both of which depend on the backend's api. Tenants are able to provision new relays, manage existing relays, and deactivate their relays. They can also manage billing information and service level. Admins are able to do this as well, but have an additional admin dashboard where they can view and manage all tenants and relays.
|
||||
|
||||
- See ./lib.md for a list of utilities
|
||||
- See ./pages.md for descriptions of each page
|
||||
- See ./components.md for a list of reusable components
|
||||
|
||||
The frontend implementation's file structure and contents should match the spec.
|
||||
|
||||
Dependencies:
|
||||
|
||||
- `applesauce` for all nostr-related functionality
|
||||
@@ -0,0 +1,25 @@
|
||||
The api allows the frontend to access the database. Most endpoints are authenticated using NIP 98.
|
||||
|
||||
## `class ApiError`
|
||||
|
||||
- This is a custom error class with an additional `status` property
|
||||
|
||||
## `function makeAuth<T>()`
|
||||
|
||||
- If the user is not currently logged in, returns undefined
|
||||
- Otherwise, builds an NIP 98 auth header with no `method` and `u` set to `VITE_API_URL`. This is non-standard, but intentional in order to prevent repeated signer authorizations.
|
||||
- This function is memoized over pubkey and expires after 10 minutes.
|
||||
- Returns the full `Authorization` header value.
|
||||
|
||||
## `function callApi<T>(method: string, path: string, body?: T)`
|
||||
|
||||
- Uses `makeAuth` to obtain a NIP 98 authorization header.
|
||||
- Calls the backend api and returns the decoded json or throws an `ApiError`.
|
||||
|
||||
## `function get(path: string)`
|
||||
|
||||
- Calls `callApi` with `get`
|
||||
|
||||
## `function post<T>(path: string, body: T)`
|
||||
|
||||
- Calls `callApi` with `post` and body
|
||||
@@ -0,0 +1,85 @@
|
||||
Nostr integration layer for the frontend. It initializes shared Nostr primitives (event store, relay pool, account manager), wires signer transport, persists local account state, and exposes reactive helpers for auth/account/profile data.
|
||||
|
||||
## Constants
|
||||
|
||||
### `API_URL`
|
||||
|
||||
- Reads `VITE_API_URL` from environment.
|
||||
|
||||
### `PLATFORM_NAME`
|
||||
|
||||
- Reads `VITE_PLATFORM_NAME` from environment.
|
||||
- Falls back to `"Caravel"`.
|
||||
|
||||
### Storage keys
|
||||
|
||||
- `caravel.accounts`: serialized account list.
|
||||
- `caravel.activeAccount`: currently selected account id.
|
||||
|
||||
## Shared singletons
|
||||
|
||||
### `eventStore`
|
||||
|
||||
- Global `EventStore` used to cache and observe Nostr events (profiles, relay lists, etc).
|
||||
|
||||
### `pool`
|
||||
|
||||
- Global `RelayPool` used for relay connections, subscriptions, and publishing.
|
||||
|
||||
### `accounts`
|
||||
|
||||
- Global `AccountManager` that holds all known local accounts and active account state.
|
||||
|
||||
## Startup wiring
|
||||
|
||||
- Registers common account types with `registerCommonAccountTypes(accounts)`.
|
||||
- Configures event loading via `createEventLoaderForStore(eventStore, pool, ...)` with lookup and extra relay seeds.
|
||||
- Binds `NostrConnectSigner` network methods to the app relay pool:
|
||||
- `subscriptionMethod = pool.subscription.bind(pool)`
|
||||
- `publishMethod = pool.publish.bind(pool)`
|
||||
|
||||
## `function restoreAccounts()`
|
||||
|
||||
- Restores serialized accounts from localStorage.
|
||||
- Ignores corrupted local data safely (catch-and-continue).
|
||||
- Restores the active account id if it still exists in the restored account set.
|
||||
|
||||
## `function activateAccount(account: IAccount)`
|
||||
|
||||
- Adds the account to `accounts`.
|
||||
- Sets it as active.
|
||||
- Persists account state to localStorage via `persistAccounts()`.
|
||||
|
||||
## `function persistAccounts()`
|
||||
|
||||
- Serializes all accounts into `caravel.accounts`.
|
||||
- Persists active account id to `caravel.activeAccount`.
|
||||
- Removes `caravel.activeAccount` if no active account exists.
|
||||
|
||||
## `function useActiveAccount()`
|
||||
|
||||
- Solid helper that returns a reactive signal for current active account.
|
||||
- Subscribes to `accounts.active$` and cleans up the subscription automatically.
|
||||
|
||||
## `function useProfilePicture(pubkey: () => string | undefined)`
|
||||
|
||||
- Reactive helper to resolve a user profile picture URL from a pubkey.
|
||||
- If pubkey is missing, clears picture to `undefined`.
|
||||
- Subscribes to profile updates in `eventStore` and maps profile to a picture via `getProfilePicture`.
|
||||
- Calls `primeProfiles([pubkey])` to proactively fetch profile data.
|
||||
- Cleans up both profile and network subscriptions when dependencies change/unmount.
|
||||
|
||||
## `function primeProfiles(pubkeys: string[])`
|
||||
|
||||
- Preloads Nostr profile events (`kind: 0`) for a set of pubkeys.
|
||||
- Deduplicates and filters invalid pubkeys.
|
||||
- Early-returns a no-op unsubscribable when no pubkeys are provided.
|
||||
- Uses currently connected relays as seeds (`pool.relays.keys()`).
|
||||
- Optionally fetches relay list events (`kind: 10002`) from seed relays to improve mailbox/outbox routing.
|
||||
- Builds optimized relay routing by:
|
||||
- including mailbox pointers from event store,
|
||||
- applying fallback relays when needed,
|
||||
- selecting optimal relays with connection limits,
|
||||
- converting to an outbox map.
|
||||
- Opens an outbox subscription for profile events and stores incoming events in `eventStore` (ignoring `EOSE`).
|
||||
- Returns an object with `unsubscribe()` that tears down both profile and mailbox-seed subscriptions.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Account
|
||||
|
||||
The account page lets an authenticated tenant manage billing settings and review invoice history.
|
||||
|
||||
```pug
|
||||
page(path="/account", auth="required", shell="app")
|
||||
header
|
||||
title My Account
|
||||
|
||||
section(id="status")
|
||||
heading Account Status
|
||||
badge tenant
|
||||
|
||||
section(id="billing")
|
||||
heading Recurring Billing
|
||||
description Enable automatic payments by providing your Nostr Wallet Connect URL.
|
||||
input(name="nwc_url", placeholder="nostr+walletconnect://...")
|
||||
button Save
|
||||
error_message(on="save_failure")
|
||||
|
||||
section(id="invoices")
|
||||
heading Invoice History
|
||||
loading_state(message="Loading invoices...")
|
||||
list(empty="No invoices yet")
|
||||
item
|
||||
status
|
||||
created_at
|
||||
bolt11
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
# Admin Relay Detail
|
||||
|
||||
The admin relay detail page exposes relay state and controls with admin-level editing and deactivation.
|
||||
|
||||
```pug
|
||||
page(path="/admin/relays/:id", auth="required", role="admin", shell="app")
|
||||
back_link(href="/admin/relays", label="Relays")
|
||||
|
||||
loading_state(message="Loading relay...")
|
||||
error_state(message="Failed to load relay.")
|
||||
|
||||
relay_detail_card(edit_href="/admin/relays/:id/edit", show_tenant=true, enforce_plan_limits=false, show_plan_actions=false)
|
||||
actions
|
||||
button(action="admin_deactivate_relay") Deactivate
|
||||
toggles
|
||||
toggle(name="policy_public_join") Public join
|
||||
toggle(name="policy_strip_signatures") Strip signatures
|
||||
toggle(name="groups_enabled") Groups
|
||||
toggle(name="management_enabled") Management API
|
||||
toggle(name="blossom_enabled") Media storage
|
||||
toggle(name="livekit_enabled") LiveKit support
|
||||
toggle(name="push_enabled") Push notifications
|
||||
|
||||
error_message(on="mutation_failure")
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# Admin Relay Edit
|
||||
|
||||
The admin relay edit page updates relay metadata and returns to the admin relay detail view.
|
||||
|
||||
```pug
|
||||
page(path="/admin/relays/:id/edit", auth="required", role="admin", shell="app")
|
||||
back_link(href="/admin/relays/:id", label="Back")
|
||||
header
|
||||
title Edit Relay (Admin)
|
||||
|
||||
loading_state(message="Loading relay...")
|
||||
error_state(message="Failed to load relay.")
|
||||
|
||||
form(action="admin_update_relay")
|
||||
field(name="name", label="Relay Name", required=true)
|
||||
field(name="subdomain", label="Subdomain", required=true)
|
||||
field(name="icon", label="Icon URL", type="url")
|
||||
field(name="description", label="Description", type="textarea")
|
||||
submit_button(default="Save Changes", loading="Saving...")
|
||||
error_message(on="submit_failure")
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
# Admin Relay List
|
||||
|
||||
The admin relay list page shows all relays with search for operators.
|
||||
|
||||
```pug
|
||||
page(path="/admin/relays", auth="required", role="admin", shell="app")
|
||||
header
|
||||
title Relays
|
||||
|
||||
controls
|
||||
input(type="search", name="query", placeholder="Search relays...")
|
||||
|
||||
loading_state(message="Loading relays...")
|
||||
error_state(message="Failed to load relays.")
|
||||
|
||||
list(empty="No relays found")
|
||||
item(link="/admin/relays/:id")
|
||||
title relay.info_name || relay.subdomain
|
||||
subtitle {relay.subdomain}.spaces.coracle.social
|
||||
tenant relay.tenant
|
||||
status relay.status
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
# Admin Tenant Detail
|
||||
|
||||
The admin tenant detail page shows tenant status and all relays for a selected tenant.
|
||||
|
||||
```pug
|
||||
page(path="/admin/tenants/:id", auth="required", role="admin", shell="app")
|
||||
back_link(href="/admin/tenants", label="Tenants")
|
||||
header
|
||||
title Tenant :id
|
||||
|
||||
loading_state(message="Loading tenant...")
|
||||
error_state(message="Failed to load tenant.")
|
||||
|
||||
section(id="status")
|
||||
heading Status
|
||||
text Current: tenant
|
||||
|
||||
section(id="relays")
|
||||
heading Relays
|
||||
list(empty="No relays")
|
||||
item(link="/admin/relays/:id")
|
||||
title relay.info_name || relay.subdomain
|
||||
subtitle {relay.subdomain}.spaces.coracle.social
|
||||
status relay.status
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
# Admin Tenant List
|
||||
|
||||
The admin tenant list page shows all tenants with profile-enriched search results.
|
||||
|
||||
```pug
|
||||
page(path="/admin/tenants", auth="required", role="admin", shell="app")
|
||||
header
|
||||
title Tenants
|
||||
|
||||
controls
|
||||
input(type="search", name="query", placeholder="Search tenants...")
|
||||
|
||||
loading_state(message="Loading tenants...")
|
||||
error_state(message="Failed to load tenants.")
|
||||
|
||||
list(empty="No tenants found")
|
||||
item(link="/admin/tenants/:pubkey")
|
||||
avatar(profile.picture)
|
||||
title profile.name || shortened_pubkey
|
||||
subtitle profile.about || "No profile bio"
|
||||
meta tenant.pubkey
|
||||
badge tenant
|
||||
```
|
||||
@@ -0,0 +1,86 @@
|
||||
# Home
|
||||
|
||||
The home page is a marketing page with CTAs to create a relay or log in (if the user isn't logged in). If the user is logged in, "Sign in" actions are replaced with "Dashboard" and route to `/relays`.
|
||||
|
||||
```pug
|
||||
page(path="/")
|
||||
nav(sticky=true)
|
||||
brand(name="Caravel", logo="/caravel.png")
|
||||
if(authenticated)
|
||||
link(href="/relays") Dashboard
|
||||
else
|
||||
link(href="/login") Sign in
|
||||
|
||||
section(id="hero")
|
||||
badge Nostr-native relay hosting
|
||||
hero
|
||||
title Your community, your relay.
|
||||
description Spin up private, managed infrastructure for your community in minutes. Full control over membership, access, and policies — no DevOps required.
|
||||
actions
|
||||
button(variant="primary", href="/relays/new") Get started free
|
||||
if(authenticated)
|
||||
button(variant="neutral", href="/relays") Dashboard
|
||||
else
|
||||
button(variant="neutral", href="/login") Sign in
|
||||
|
||||
section(id="features")
|
||||
heading
|
||||
title Everything you need
|
||||
description Caravel takes care of the infrastructure so you can focus on building your community.
|
||||
grid(columns=3)
|
||||
card(icon="hosting")
|
||||
title Managed hosting
|
||||
description We handle uptime, backups, and updates. Your relay stays online so your community never misses a beat.
|
||||
card(icon="membership")
|
||||
title Membership control
|
||||
description Approve members with Nostr pubkeys. Keep your space invite-only or open — you decide.
|
||||
card(icon="shield")
|
||||
title Access policies
|
||||
description Fine-grained write and read permissions. Moderate content without touching any server config.
|
||||
card(icon="storage")
|
||||
title Blossom storage
|
||||
description Attach media files to your relay with integrated Blossom server support on paid plans.
|
||||
card(icon="video")
|
||||
title LiveKit video
|
||||
description Built-in video room support via LiveKit. Host voice and video calls directly within your community.
|
||||
card(icon="lightning")
|
||||
title Pay with sats
|
||||
description Lightning-native billing. No credit cards, no bank accounts — just sats, straight from your wallet.
|
||||
|
||||
section(id="connect")
|
||||
heading
|
||||
title Connect with your community
|
||||
description Once your relay is live, these Nostr-native platforms let your members connect, chat, and collaborate — all powered by your relay.
|
||||
grid(columns=2)
|
||||
product(name="Flotilla", href="https://flotilla.social", domain="flotilla.social")
|
||||
description A community platform built on Nostr. Flotilla gives your members channels, threads, and a rich social experience — all connected to your relay.
|
||||
bullets
|
||||
item Nostr-native channels & threads
|
||||
item Works directly with your relay
|
||||
item Open source & self-sovereign
|
||||
product(name="Chachi", href="https://chachi.chat", domain="chachi.chat")
|
||||
description A group chat app built on top of Nostr. Chachi makes it easy for your community to have real-time conversations, all flowing through your own relay.
|
||||
bullets
|
||||
item Real-time group messaging
|
||||
item Bring your own relay
|
||||
item No accounts — just your Nostr key
|
||||
|
||||
section(id="pricing")
|
||||
heading
|
||||
title Simple pricing
|
||||
description Pay in sats. Upgrade or cancel any time.
|
||||
pricing_table(cta_href="/relays/new")
|
||||
|
||||
section(id="cta")
|
||||
heading
|
||||
title Ready to launch your relay?
|
||||
description Join communities already running on Caravel. Set up in minutes, pay in sats.
|
||||
button(variant="primary", href="/relays/new") Create your relay
|
||||
|
||||
footer
|
||||
brand(name="Caravel", logo="/caravel.png")
|
||||
copyright © {currentYear} Caravel. Built on Nostr.
|
||||
links
|
||||
link(href="https://flotilla.social") Flotilla
|
||||
link(href="https://chachi.chat") Chachi
|
||||
```
|
||||
@@ -0,0 +1,67 @@
|
||||
# Login
|
||||
|
||||
The login page authenticates users with Nostr via extension, remote signer (NIP-46), or key material, then redirects to `/relays`.
|
||||
|
||||
```pug
|
||||
page(path="/login")
|
||||
hero
|
||||
badge Secure Nostr Login
|
||||
title Welcome back
|
||||
description Connect your Nostr account to manage relay hosting, billing, and access in one place.
|
||||
bullets
|
||||
item Own your identity with cryptographic sign-in.
|
||||
item No passwords or email required.
|
||||
item Get fast access to your relays.
|
||||
|
||||
auth_card
|
||||
section(id="method_select")
|
||||
title Log in / Sign up
|
||||
description Use any Nostr signer method. New users are automatically onboarded.
|
||||
tabs
|
||||
tab(id="nip07") Extension
|
||||
tab(id="nip46") Signer
|
||||
tab(id="key") Key
|
||||
|
||||
when(tab="nip07")
|
||||
button Continue with extension
|
||||
|
||||
when(tab="nip46")
|
||||
button Continue with signer
|
||||
|
||||
when(tab="key")
|
||||
button Continue with key
|
||||
|
||||
screen(id="nip46")
|
||||
back_button Back
|
||||
title Log in with signer
|
||||
tabs
|
||||
tab(id="qr") Use QR Code
|
||||
tab(id="paste") Paste Link
|
||||
when(tab="qr")
|
||||
qr_code
|
||||
copyable_input(name="nostrconnect_uri")
|
||||
when(tab="paste")
|
||||
input(name="bunker_url", placeholder="bunker://...")
|
||||
button Scan QR code
|
||||
button Connect to Signer
|
||||
|
||||
screen(id="key")
|
||||
back_button Back
|
||||
title Log in with key
|
||||
tabs
|
||||
tab(id="plaintext") Plaintext
|
||||
tab(id="encrypted") Encrypted
|
||||
when(tab="plaintext")
|
||||
input(name="nsec", placeholder="nsec1...")
|
||||
when(tab="encrypted")
|
||||
input(name="ncryptsec", placeholder="ncryptsec1...")
|
||||
input(type="password", name="password", placeholder="Password")
|
||||
button Log in
|
||||
|
||||
error_message
|
||||
|
||||
help_text Having trouble? Make sure your signer is unlocked and connected.
|
||||
modal(id="scanner")
|
||||
title Scan QR Code
|
||||
camera_preview
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
# Relay Detail
|
||||
|
||||
The relay detail page shows relay status and settings for an authenticated tenant, with inline toggles and plan changes.
|
||||
|
||||
```pug
|
||||
page(path="/relays/:id", auth="required", shell="app")
|
||||
back_link(href="/relays", label="Relays")
|
||||
|
||||
loading_state(message="Loading relay...")
|
||||
error_state(message="Failed to load relay.")
|
||||
|
||||
relay_detail_card(edit_href="/relays/:id/edit")
|
||||
actions
|
||||
button(action="deactivate_relay") Deactivate
|
||||
toggles
|
||||
toggle(name="policy_public_join") Public join
|
||||
toggle(name="policy_strip_signatures") Strip signatures
|
||||
toggle(name="groups_enabled") Groups
|
||||
toggle(name="management_enabled") Management API
|
||||
toggle(name="blossom_enabled") Media storage
|
||||
toggle(name="livekit_enabled") LiveKit support
|
||||
toggle(name="push_enabled") Push notifications
|
||||
plan_selector(action="update_plan")
|
||||
member_count(source="relay_url")
|
||||
|
||||
error_message(on="mutation_failure")
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# Relay Edit
|
||||
|
||||
The relay edit page lets an authenticated tenant update relay metadata and returns to relay detail on success.
|
||||
|
||||
```pug
|
||||
page(path="/relays/:id/edit", auth="required", shell="app")
|
||||
back_link(href="/relays/:id", label="Back")
|
||||
header
|
||||
title Edit Relay
|
||||
|
||||
loading_state(message="Loading relay...")
|
||||
error_state(message="Failed to load relay.")
|
||||
|
||||
form(action="update_tenant_relay")
|
||||
field(name="name", label="Relay Name", required=true)
|
||||
field(name="subdomain", label="Subdomain", required=true)
|
||||
field(name="icon", label="Icon URL", type="url")
|
||||
field(name="description", label="Description", type="textarea")
|
||||
submit_button(default="Save Changes", loading="Saving...")
|
||||
error_message(on="submit_failure")
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
# Relay List
|
||||
|
||||
The relay list page shows an authenticated tenant's relays with search and status filtering.
|
||||
|
||||
```pug
|
||||
page(path="/relays", auth="required", shell="app")
|
||||
header
|
||||
title My Relays
|
||||
button(href="/relays/new") Add Relay
|
||||
|
||||
controls
|
||||
input(type="search", name="query", placeholder="Search by name or subdomain")
|
||||
select(name="status")
|
||||
option(value="all") All statuses
|
||||
option(value="active") Active
|
||||
option(value="pending") Pending
|
||||
option(value="deactivated") Deactivated
|
||||
option(value="provisioning_failed") Provisioning failed
|
||||
option(value="suspended") Suspended
|
||||
|
||||
loading_state(message="Loading relays...")
|
||||
error_state(message="Failed to load relays.")
|
||||
|
||||
list(empty="No relays found")
|
||||
item(link="/relays/:id")
|
||||
title relay.info_name || relay.subdomain
|
||||
subtitle https://{relay.subdomain}.spaces.coracle.social
|
||||
status relay.status
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# Relay New
|
||||
|
||||
The new relay page lets an authenticated tenant configure and create a relay, then navigates to its detail page.
|
||||
|
||||
```pug
|
||||
page(path="/relays/new", auth="required", shell="app")
|
||||
header
|
||||
title New Relay
|
||||
|
||||
form(action="create_tenant_relay")
|
||||
field(name="name", label="Relay Name", required=true, placeholder="My Community")
|
||||
field(name="subdomain", label="Subdomain", required=true, suffix=".spaces.coracle.social", placeholder="my-community")
|
||||
field(name="icon", label="Icon URL", type="url", placeholder="https://example.com/icon.png")
|
||||
field(name="description", label="Description", type="textarea", placeholder="A community for...")
|
||||
|
||||
field(name="plan", label="Plan")
|
||||
option(value="free")
|
||||
title Free
|
||||
price Free
|
||||
members Up to 10 members
|
||||
option(value="basic")
|
||||
title Basic
|
||||
price 10,000 sats/mo
|
||||
members Up to 100 members
|
||||
option(value="growth")
|
||||
title Growth
|
||||
price 50,000 sats/mo
|
||||
members Unlimited members
|
||||
|
||||
tooltip_error(field="subdomain")
|
||||
submit_button(default="Create Relay", loading="Creating...")
|
||||
```
|
||||
@@ -1,5 +1,6 @@
|
||||
import { A } from "@solidjs/router"
|
||||
import PricingTable from "../components/PricingTable"
|
||||
import { useActiveAccount } from "../lib/nostr"
|
||||
|
||||
function CheckIcon() {
|
||||
return (
|
||||
@@ -20,6 +21,8 @@ function ExternalLinkIcon() {
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const account = useActiveAccount()
|
||||
|
||||
return (
|
||||
<div class="min-h-screen bg-white text-gray-900 overflow-x-hidden">
|
||||
|
||||
@@ -31,10 +34,10 @@ export default function Home() {
|
||||
Caravel
|
||||
</div>
|
||||
<A
|
||||
href="/login"
|
||||
href={account() ? "/relays" : "/login"}
|
||||
class="text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
Sign in
|
||||
{account() ? "Dashboard" : "Sign in"}
|
||||
</A>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -70,10 +73,10 @@ export default function Home() {
|
||||
<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>
|
||||
</A>
|
||||
<A
|
||||
href="/login"
|
||||
href={account() ? "/relays" : "/login"}
|
||||
class="inline-flex items-center gap-2 py-3 px-8 border border-gray-200 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 transition-all"
|
||||
>
|
||||
Sign in
|
||||
{account() ? "Dashboard" : "Sign in"}
|
||||
</A>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user