From d8443410cd7f95cfdaa8c06092b345fa50675462 Mon Sep 17 00:00:00 2001 From: highperfocused Date: Fri, 9 May 2025 15:31:46 +0200 Subject: [PATCH] refactor: simplify SetupPage and implement Connector component for relay connection --- app/setup/page.tsx | 101 +---------------------- components/connector.tsx | 170 +++++++++++++++++++++++++++++++++++++++ components/ui/alert.tsx | 66 +++++++++++++++ 3 files changed, 239 insertions(+), 98 deletions(-) create mode 100644 components/connector.tsx create mode 100644 components/ui/alert.tsx diff --git a/app/setup/page.tsx b/app/setup/page.tsx index 6f2d199..a13a4a4 100644 --- a/app/setup/page.tsx +++ b/app/setup/page.tsx @@ -1,16 +1,10 @@ "use client" -import { useState, useEffect } from "react" +import { useEffect } from "react" import { useRouter } from "next/navigation" -import { Server, ArrowRight } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" +import Connector from "@/components/connector" export default function SetupPage() { - const [relayUrl, setRelayUrl] = useState("") - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState("") const router = useRouter() // Check if user already has a relay configured - if yes, redirect to dashboard @@ -21,96 +15,7 @@ export default function SetupPage() { } }, [router]) - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - setIsLoading(true) - setError("") - - // Validate the URL format - try { - // Simple validation for websocket URL format - if (!relayUrl.trim()) { - throw new Error("Please enter a relay URL") - } - - // Check if it's a valid URL - const url = new URL(relayUrl) - - // Check if it's a ws:// or wss:// protocol - if (!url.protocol.match(/^wss?:$/)) { - throw new Error("Relay URL must use WebSocket protocol (ws:// or wss://)") - } - - // Save to localStorage - localStorage.setItem("relayUrl", relayUrl.trim()) - - // Redirect to dashboard - router.push("/dashboard") - } catch (err: any) { - setError(err.message || "Invalid relay URL format") - setIsLoading(false) - } - } - return ( -
-
-
-
- - NOSTR Relay Manager -
-
-
- -
- - - Connect to Your Relay - - Enter your relay's WebSocket URL to connect to your NOSTR relay - - -
- -
-
- - setRelayUrl(e.target.value)} - required - /> - {error && ( -

{error}

- )} -

- This should be the WebSocket URL of your NOSTR relay, starting with ws:// or wss:// -

-
-
-
- - - -
-
-
- -
-
-

- © {new Date().getFullYear()} NOSTR Relay Manager -

-
-
-
+ ) } \ No newline at end of file diff --git a/components/connector.tsx b/components/connector.tsx new file mode 100644 index 0000000..81c3b69 --- /dev/null +++ b/components/connector.tsx @@ -0,0 +1,170 @@ +"use client" + +import { useState, useEffect } from "react" +import { AlertCircle, CheckCircle2 } from "lucide-react" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { useRouter } from "next/navigation" + +interface RelayInfo { + name?: string + description?: string + pubkey?: string + contact?: string + supported_nips?: number[] + software?: string + version?: string + [key: string]: any +} + +export default function Connector() { + const [relayUrl, setRelayUrl] = useState("") + // const [url, setUrl] = useState("") + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [relayInfo, setRelayInfo] = useState(null) + const router = useRouter() + + useEffect(() => { + const fetchRelayInfo = async () => { + if (!relayUrl) { + setRelayInfo(null) + setError(null) + return + } + + setIsLoading(true) + setError(null) + + try { + // Convert WebSocket URL to HTTP/HTTPS + const httpUrl = relayUrl.replace(/^wss?:\/\//i, (match) => (match === "ws://" ? "http://" : "https://")) + + const response = await fetch(httpUrl, { + headers: { + Accept: "application/nostr+json", + }, + }) + + if (!response.ok) { + throw new Error(`Failed to fetch relay info: ${response.status}`) + } + + const data = await response.json() + setRelayInfo(data) + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch relay information") + setRelayInfo(null) + } finally { + setIsLoading(false) + } + } + + // Debounce the fetch to avoid too many requests + const timeoutId = setTimeout(() => { + fetchRelayInfo() + }, 500) + + return () => clearTimeout(timeoutId) + }, [relayUrl]) + + const handleConnect = (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError("") + + // Validate the URL format + try { + // Simple validation for websocket URL format + if (!relayUrl.trim()) { + throw new Error("Please enter a relay URL") + } + + // Check if it's a valid URL + const url = new URL(relayUrl) + + // Check if it's a ws:// or wss:// protocol + if (!url.protocol.match(/^wss?:$/)) { + throw new Error("Relay URL must use WebSocket protocol (ws:// or wss://)") + } + + // Save to localStorage + localStorage.setItem("relayUrl", relayUrl.trim()) + + // Redirect to dashboard + router.push("/dashboard") + } catch (err: any) { + setError(err.message || "Invalid relay URL format") + setIsLoading(false) + } + } + + return ( + + + Nostr Relay Connector + Enter a WebSocket URL to connect to a Nostr relay + + +
+ + setRelayUrl(e.target.value)} + className="w-full" + /> +

Example: wss://relay.damus.io

+
+ + {isLoading && ( +
+
+ Checking relay... +
+ )} + + {error && ( + + + {error} + + )} + + {relayInfo && ( +
+ + + Successfully connected to relay + + +
+
+ Name: {relayInfo.name || "N/A"} +
+ {relayInfo.description && ( +
+ Description: {relayInfo.description} +
+ )} + {relayInfo.supported_nips && ( +
+ Supported NIPs: {relayInfo.supported_nips.join(", ")} +
+ )} +
+ + +
+ )} +
+
+ ) +} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription }