refactor: simplify SetupPage and implement Connector component for relay connection
This commit is contained in:
@@ -1,16 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { Server, ArrowRight } from "lucide-react"
|
import Connector from "@/components/connector"
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
|
||||||
|
|
||||||
export default function SetupPage() {
|
export default function SetupPage() {
|
||||||
const [relayUrl, setRelayUrl] = useState("")
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [error, setError] = useState("")
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// Check if user already has a relay configured - if yes, redirect to dashboard
|
// Check if user already has a relay configured - if yes, redirect to dashboard
|
||||||
@@ -21,96 +15,7 @@ export default function SetupPage() {
|
|||||||
}
|
}
|
||||||
}, [router])
|
}, [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 (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<Connector />
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
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