refactor: simplify SetupPage and implement Connector component for relay connection
This commit is contained in:
@@ -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
170
components/connector.tsx
Normal 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
66
components/ui/alert.tsx
Normal 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 }
|
Reference in New Issue
Block a user