diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx
new file mode 100644
index 0000000..a1aeea2
--- /dev/null
+++ b/frontend/src/components/Toast.tsx
@@ -0,0 +1,74 @@
+import { Show, createEffect, createSignal, onCleanup } from "solid-js"
+
+type ToastProps = {
+ message?: string
+ duration?: number
+ onClear?: () => void
+}
+
+export default function Toast(props: ToastProps) {
+ const [visible, setVisible] = createSignal(false)
+
+ let hideTimer: number | undefined
+ let clearTimer: number | undefined
+ let rafOne: number | undefined
+ let rafTwo: number | undefined
+
+ function clearTimers() {
+ if (hideTimer) window.clearTimeout(hideTimer)
+ if (clearTimer) window.clearTimeout(clearTimer)
+ if (rafOne) window.cancelAnimationFrame(rafOne)
+ if (rafTwo) window.cancelAnimationFrame(rafTwo)
+ hideTimer = undefined
+ clearTimer = undefined
+ rafOne = undefined
+ rafTwo = undefined
+ }
+
+ createEffect(() => {
+ const message = props.message?.trim()
+ clearTimers()
+
+ if (!message) {
+ setVisible(false)
+ return
+ }
+
+ setVisible(false)
+ rafOne = window.requestAnimationFrame(() => {
+ rafTwo = window.requestAnimationFrame(() => {
+ setVisible(true)
+ rafOne = undefined
+ rafTwo = undefined
+ })
+ })
+
+ hideTimer = window.setTimeout(() => {
+ setVisible(false)
+ clearTimer = window.setTimeout(() => {
+ props.onClear?.()
+ clearTimer = undefined
+ }, 250)
+ hideTimer = undefined
+ }, props.duration ?? 10_000)
+ })
+
+ onCleanup(() => {
+ clearTimers()
+ })
+
+ return (
+
+
+ {props.message}
+
+
+ )
+}
diff --git a/frontend/src/pages/relays/RelayList.tsx b/frontend/src/pages/relays/RelayList.tsx
index bb209a4..1ce045f 100644
--- a/frontend/src/pages/relays/RelayList.tsx
+++ b/frontend/src/pages/relays/RelayList.tsx
@@ -37,7 +37,7 @@ export default function RelayList() {
{relay.name}
-
{relay.subdomain}
+
https://{relay.subdomain}.spaces.coracle.social
{relay.status}
diff --git a/frontend/src/pages/relays/RelayNew.tsx b/frontend/src/pages/relays/RelayNew.tsx
index 09a1ad8..f923225 100644
--- a/frontend/src/pages/relays/RelayNew.tsx
+++ b/frontend/src/pages/relays/RelayNew.tsx
@@ -1,6 +1,6 @@
-import { createSignal } from "solid-js"
+import { Show, createSignal } from "solid-js"
import { useNavigate } from "@solidjs/router"
-import { createTenantRelay, listTenantRelays } from "../../lib/api"
+import { createTenantRelay } from "../../lib/api"
const PLANS = [
{ id: "free", label: "Free", price: 0, members: "Up to 10", blossom: false, livekit: false },
@@ -28,38 +28,31 @@ export default function RelayNew() {
const [description, setDescription] = createSignal("")
const [plan, setPlan] = createSignal("free")
const [submitting, setSubmitting] = createSignal(false)
- const [error, setError] = createSignal("")
+ const [subdomainError, setSubdomainError] = createSignal("")
function handleNameInput(value: string) {
setName(value)
setSubdomain(slugify(value))
+ setSubdomainError("")
}
async function handleSubmit(e: Event) {
e.preventDefault()
- setError("")
+ setSubdomainError("")
setSubmitting(true)
try {
- const normalizedSubdomain = slugify(subdomain())
- const existingRelays = await listTenantRelays()
- const hasDuplicate = existingRelays.some(relay => relay.subdomain.toLowerCase() === normalizedSubdomain.toLowerCase())
-
- if (hasDuplicate) {
- throw new Error("You already have a relay with this subdomain")
- }
-
const relay = await createTenantRelay({
name: name().trim(),
- subdomain: normalizedSubdomain,
+ subdomain: slugify(subdomain()),
icon: icon().trim(),
description: description().trim(),
plan: plan(),
})
navigate(`/relays/${relay.id}`)
} catch (e) {
- setError(e instanceof Error ? e.message : "Failed to create relay")
+ setSubdomainError(e instanceof Error ? e.message : "Failed to create relay")
} finally {
setSubmitting(false)
}
@@ -69,12 +62,6 @@ export default function RelayNew() {