refactor: simplify SetupPage and implement Connector component for relay connection

This commit is contained in:
2025-05-09 15:31:46 +02:00
parent f06709b3f9
commit d8443410cd
3 changed files with 239 additions and 98 deletions

View File

@@ -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 (
<div className="min-h-screen flex flex-col">
<header className="border-b">
<div className="container flex h-16 items-center px-4 md:px-6 mx-auto">
<div className="flex items-center gap-2">
<Server className="h-6 w-6" />
<span className="text-lg font-bold">NOSTR Relay Manager</span>
</div>
</div>
</header>
<main className="flex-1 flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-2xl">Connect to Your Relay</CardTitle>
<CardDescription>
Enter your relay's WebSocket URL to connect to your NOSTR relay
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit}>
<CardContent>
<div className="grid gap-6">
<div className="grid gap-3">
<label className="text-sm font-medium leading-none" htmlFor="relay-url">
Relay WebSocket URL
</label>
<Input
id="relay-url"
placeholder="wss://your-relay.com"
value={relayUrl}
onChange={(e) => setRelayUrl(e.target.value)}
required
/>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
<p className="text-xs text-muted-foreground">
This should be the WebSocket URL of your NOSTR relay, starting with ws:// or wss://
</p>
</div>
</div>
</CardContent>
<CardFooter>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Connecting..." : "Connect to Relay"}
{!isLoading && <ArrowRight className="ml-2 h-4 w-4" />}
</Button>
</CardFooter>
</form>
</Card>
</main>
<footer className="border-t py-6">
<div className="container flex items-center justify-center px-4 md:px-6 mx-auto">
<p className="text-sm text-muted-foreground">
© {new Date().getFullYear()} NOSTR Relay Manager
</p>
</div>
</footer>
</div>
<Connector />
)
}

170
components/connector.tsx Normal file
View File

@@ -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<string | null>(null)
const [relayInfo, setRelayInfo] = useState<RelayInfo | null>(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 (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>Nostr Relay Connector</CardTitle>
<CardDescription>Enter a WebSocket URL to connect to a Nostr relay</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label htmlFor="websocket-url" className="text-sm font-medium">
WebSocket URL
</label>
<Input
id="websocket-url"
placeholder="wss://relay.example.com"
value={relayUrl}
onChange={(e) => setRelayUrl(e.target.value)}
className="w-full"
/>
<p className="text-xs text-muted-foreground">Example: wss://relay.damus.io</p>
</div>
{isLoading && (
<div className="flex items-center justify-center py-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
<span className="ml-2 text-sm">Checking relay...</span>
</div>
)}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{relayInfo && (
<div className="space-y-4">
<Alert variant="default" className="bg-green-50 border-green-200">
<CheckCircle2 className="h-4 w-4 text-green-600" />
<AlertDescription className="text-green-600">Successfully connected to relay</AlertDescription>
</Alert>
<div className="space-y-2 text-sm">
<div>
<span className="font-medium">Name:</span> {relayInfo.name || "N/A"}
</div>
{relayInfo.description && (
<div>
<span className="font-medium">Description:</span> {relayInfo.description}
</div>
)}
{relayInfo.supported_nips && (
<div>
<span className="font-medium">Supported NIPs:</span> {relayInfo.supported_nips.join(", ")}
</div>
)}
</div>
<Button className="w-full" onClick={handleConnect}>
Connect as Admin
</Button>
</div>
)}
</CardContent>
</Card>
)
}

66
components/ui/alert.tsx Normal file
View File

@@ -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<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }