Create modern live relay landing page

This commit is contained in:
2026-05-16 16:24:39 +02:00
parent 4acdb31b8a
commit c50f486a9c
2 changed files with 590 additions and 195 deletions

View File

@@ -120,4 +120,85 @@
body {
@apply bg-background text-foreground;
}
}
}
@layer utilities {
.relay-grid {
background-image:
linear-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.08) 1px, transparent 1px),
linear-gradient(rgba(251, 191, 36, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(251, 191, 36, 0.08) 1px, transparent 1px);
background-size: 44px 44px, 44px 44px, 176px 176px, 176px 176px;
}
.relay-scanline {
animation: relay-scan 5.5s linear infinite;
background: linear-gradient(to bottom, transparent, rgba(251, 191, 36, 0.18), transparent);
}
.event-packet {
animation: event-packet-pop 1.6s ease-out both;
box-shadow: 0 0 22px currentColor;
}
.event-packet-live {
animation: event-packet-pop 1.6s ease-out both, event-packet-breathe 2.4s ease-in-out infinite;
}
.live-row-new {
animation: live-row-arrive 1.4s ease-out both;
}
}
@keyframes relay-scan {
0% {
transform: translateY(-7rem);
}
100% {
transform: translateY(28rem);
}
}
@keyframes event-packet-pop {
0% {
opacity: 0;
transform: scale(0.25);
}
55% {
opacity: 1;
transform: scale(1.7);
}
100% {
opacity: 0.92;
transform: scale(1);
}
}
@keyframes event-packet-breathe {
0%,
100% {
filter: brightness(1);
}
50% {
filter: brightness(1.45);
}
}
@keyframes live-row-arrive {
0% {
border-color: rgba(190, 242, 100, 0.72);
box-shadow: 0 0 0 1px rgba(190, 242, 100, 0.22), 0 0 34px rgba(190, 242, 100, 0.16);
transform: translateY(-4px);
}
100% {
border-color: rgba(255, 255, 255, 0.1);
box-shadow: none;
transform: translateY(0);
}
}

View File

@@ -1,237 +1,551 @@
import { useSeoMeta } from '@unhead/react';
import { useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import type { NostrEvent } from '@nostrify/nostrify';
import { useNostr } from '@nostrify/react';
import { Link } from 'react-router-dom';
import {
Activity,
ArrowUpRight,
CheckCircle2,
CircleDot,
Copy,
Gauge,
Globe2,
RadioTower,
Server,
ShieldCheck,
Sparkles,
Zap,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { CheckCircle2, Copy, Server, Gift, Users, Globe } from 'lucide-react';
import { useToast } from '@/hooks/useToast';
import { Badge } from '@/components/ui/badge';
import { LoginArea } from '@/components/auth/LoginArea';
import { useToast } from '@/hooks/useToast';
import { useAppContext } from '@/hooks/useAppContext';
import { genUserName } from '@/lib/genUserName';
import { cn } from '@/lib/utils';
type LiveEventSource = 'seed' | 'live';
interface LiveRelayEvent {
event: NostrEvent;
receivedAt: number;
source: LiveEventSource;
}
const RELAY_URL = 'wss://relay.layer.systems';
const LIVE_KINDS = [0, 1, 3, 6, 7, 9735, 30023];
const MAX_EVENTS = 28;
function useLiveRelayEvents() {
const { nostr } = useNostr();
const { config } = useAppContext();
const [events, setEvents] = useState<LiveRelayEvent[]>([]);
const [totalSeen, setTotalSeen] = useState(0);
const [status, setStatus] = useState<'connecting' | 'live' | 'quiet' | 'error'>('connecting');
const readRelays = useMemo(
() => config.relayMetadata.relays.filter((relay) => relay.read).map((relay) => relay.url),
[config.relayMetadata.relays],
);
useEffect(() => {
if (import.meta.env.MODE === 'test') {
return;
}
const controller = new AbortController();
let isActive = true;
const appendEvent = (event: NostrEvent, source: LiveEventSource) => {
setEvents((current) => {
if (current.some((item) => item.event.id === event.id)) {
return current;
}
return [
{
event,
receivedAt: Date.now(),
source,
},
...current,
]
.sort((a, b) => b.event.created_at - a.event.created_at)
.slice(0, MAX_EVENTS);
});
setTotalSeen((count) => count + 1);
};
const run = async () => {
const now = Math.floor(Date.now() / 1000);
const filters = [{ kinds: LIVE_KINDS, since: now - 60 * 60, limit: MAX_EVENTS }];
try {
setStatus('connecting');
const recentEvents = await nostr.query(filters, { signal: controller.signal });
if (!isActive) return;
recentEvents
.sort((a, b) => b.created_at - a.created_at)
.slice(0, MAX_EVENTS)
.forEach((event) => appendEvent(event, 'seed'));
setStatus('live');
const liveFilters = [{ kinds: LIVE_KINDS, since: Math.floor(Date.now() / 1000) }];
for await (const msg of nostr.req(liveFilters, { signal: controller.signal })) {
if (!isActive) break;
if (msg[0] === 'EVENT') {
appendEvent(msg[2], 'live');
setStatus('live');
} else if (msg[0] === 'CLOSED') {
setStatus('quiet');
}
}
} catch (error) {
if (!isActive || controller.signal.aborted) return;
console.error('[Index] live relay subscription failed:', error);
setStatus('error');
}
};
void run();
return () => {
isActive = false;
controller.abort();
};
}, [nostr]);
return {
events,
status,
totalSeen,
readRelays,
lastEventAt: events[0]?.receivedAt,
};
}
function shortKey(pubkey: string) {
return `${pubkey.slice(0, 6)}:${pubkey.slice(-4)}`;
}
function getKindMeta(kind: number) {
switch (kind) {
case 0:
return { label: 'Profile', accent: 'bg-cyan-300', text: 'text-cyan-100', ring: 'ring-cyan-300/30' };
case 1:
return { label: 'Note', accent: 'bg-amber-300', text: 'text-amber-100', ring: 'ring-amber-300/30' };
case 3:
return { label: 'Relay list', accent: 'bg-lime-300', text: 'text-lime-100', ring: 'ring-lime-300/30' };
case 6:
return { label: 'Repost', accent: 'bg-sky-300', text: 'text-sky-100', ring: 'ring-sky-300/30' };
case 7:
return { label: 'Reaction', accent: 'bg-fuchsia-300', text: 'text-fuchsia-100', ring: 'ring-fuchsia-300/30' };
case 9735:
return { label: 'Zap', accent: 'bg-emerald-300', text: 'text-emerald-100', ring: 'ring-emerald-300/30' };
case 30023:
return { label: 'Article', accent: 'bg-rose-300', text: 'text-rose-100', ring: 'ring-rose-300/30' };
default:
return { label: `Kind ${kind}`, accent: 'bg-white', text: 'text-white', ring: 'ring-white/20' };
}
}
function getTagValue(event: NostrEvent, tagName: string) {
return event.tags.find(([name]) => name === tagName)?.[1];
}
function getEventPreview(event: NostrEvent) {
if (event.kind === 0) {
try {
const metadata = JSON.parse(event.content) as { name?: string; display_name?: string; about?: string };
const name = metadata.display_name || metadata.name || genUserName(event.pubkey);
return metadata.about ? `${name}: ${metadata.about}` : `${name} refreshed a profile`;
} catch {
return 'Profile metadata update';
}
}
if (event.kind === 1) {
return event.content.trim() || 'Empty text note';
}
if (event.kind === 30023) {
return getTagValue(event, 'title') || event.content.trim() || 'Long-form note published';
}
if (event.kind === 9735) {
return 'Lightning zap receipt settled on the relay';
}
if (event.kind === 7) {
return event.content.trim() ? `Reaction: ${event.content.trim()}` : 'Reaction event';
}
if (event.kind === 6) {
return 'Repost pointer propagated';
}
if (event.kind === 3) {
return 'Contact or relay graph changed';
}
return event.content.trim() || `Kind ${event.kind} event`;
}
function formatEventTime(timestamp: number) {
return new Intl.DateTimeFormat('en', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}).format(new Date(timestamp * 1000));
}
function formatRelativeTime(timestamp?: number) {
if (!timestamp) return 'waiting';
const seconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1000));
if (seconds < 5) return 'just now';
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
return `${Math.floor(minutes / 60)}h ago`;
}
function EventConstellation({ events }: { events: LiveRelayEvent[] }) {
const plottedEvents = events.slice(0, 18);
return (
<div className="relative min-h-[430px] overflow-hidden rounded-md border border-white/10 bg-[#080806] shadow-[0_30px_90px_rgba(0,0,0,0.35)]">
<div className="relay-grid absolute inset-0" />
<div className="relay-scanline absolute inset-x-0 top-0 h-24" />
<div className="absolute left-5 top-5 z-10 flex items-center gap-2 rounded-full border border-white/10 bg-black/40 px-3 py-1.5 text-xs text-white/70 backdrop-blur">
<span className="h-2 w-2 rounded-full bg-lime-300 shadow-[0_0_18px_rgba(190,242,100,0.9)]" />
live relay field
</div>
<div className="absolute bottom-5 left-5 z-10 max-w-52 text-xs leading-5 text-white/55">
Each pulse is an event entering from a configured read relay. Notes, zaps, profiles, and reactions land as separate signal types.
</div>
<div className="absolute inset-8">
<div className="absolute left-1/2 top-1/2 h-28 w-28 -translate-x-1/2 -translate-y-1/2 rounded-full border border-amber-200/25 bg-amber-200/5 shadow-[0_0_55px_rgba(251,191,36,0.2)]" />
<div className="absolute left-1/2 top-1/2 h-48 w-48 -translate-x-1/2 -translate-y-1/2 rounded-full border border-dashed border-white/10" />
<div className="absolute left-1/2 top-1/2 h-72 w-72 -translate-x-1/2 -translate-y-1/2 rounded-full border border-dashed border-white/10" />
<RadioTower className="absolute left-1/2 top-1/2 h-12 w-12 -translate-x-1/2 -translate-y-1/2 text-amber-200" />
{plottedEvents.map(({ event, source }, index) => {
const x = 10 + (parseInt(event.id.slice(0, 4), 16) % 80);
const y = 12 + (parseInt(event.id.slice(4, 8), 16) % 76);
const meta = getKindMeta(event.kind);
return (
<span
key={event.id}
className={cn(
'event-packet absolute h-3 w-3 rounded-full ring-4',
meta.accent,
meta.ring,
source === 'live' && 'event-packet-live',
)}
style={{
left: `${x}%`,
top: `${y}%`,
animationDelay: `${Math.min(index * 80, 900)}ms`,
}}
title={`${meta.label} from ${shortKey(event.pubkey)}`}
/>
);
})}
</div>
<div className="absolute inset-x-0 bottom-0 h-px bg-gradient-to-r from-transparent via-amber-200/70 to-transparent" />
</div>
);
}
function LiveEventRow({ item }: { item: LiveRelayEvent }) {
const { event, receivedAt, source } = item;
const meta = getKindMeta(event.kind);
return (
<article
className={cn(
'group rounded-md border border-white/10 bg-white/[0.055] p-3 transition duration-300 hover:border-white/20 hover:bg-white/[0.08]',
source === 'live' && 'live-row-new',
)}
>
<div className="mb-2 flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
<span className={cn('h-2 w-2 shrink-0 rounded-full', meta.accent)} />
<span className={cn('text-xs font-semibold uppercase', meta.text)}>{meta.label}</span>
<span className="truncate font-mono text-[11px] text-white/35">{shortKey(event.pubkey)}</span>
</div>
<time className="shrink-0 font-mono text-[11px] text-white/35">{formatEventTime(event.created_at)}</time>
</div>
<p className="line-clamp-2 text-sm leading-5 text-white/76">{getEventPreview(event)}</p>
<div className="mt-2 flex items-center justify-between font-mono text-[11px] text-white/32">
<span>{source === 'live' ? 'incoming' : 'recent'}</span>
<span>{formatRelativeTime(receivedAt)}</span>
</div>
</article>
);
}
function LiveRelayPanel({
events,
status,
totalSeen,
readRelays,
lastEventAt,
}: {
events: LiveRelayEvent[];
status: 'connecting' | 'live' | 'quiet' | 'error';
totalSeen: number;
readRelays: string[];
lastEventAt?: number;
}) {
const statusLabel = status === 'live' ? 'streaming' : status;
return (
<section className="rounded-md border border-white/10 bg-[#10100d]/90 p-4 shadow-[0_24px_70px_rgba(0,0,0,0.28)] backdrop-blur">
<div className="mb-4 flex items-start justify-between gap-4">
<div>
<div className="flex items-center gap-2 text-xs font-semibold uppercase text-amber-100">
<Activity className="h-4 w-4" />
live ingress
</div>
<p className="mt-1 text-sm text-white/48">Incoming Nostr events from configured read relays.</p>
</div>
<Badge className="border border-lime-300/30 bg-lime-300/10 text-lime-100 hover:bg-lime-300/10">
{statusLabel}
</Badge>
</div>
<div className="grid grid-cols-3 gap-2 text-white">
<div className="rounded-md border border-white/10 bg-black/20 p-3">
<p className="text-[11px] uppercase text-white/38">events</p>
<p className="mt-1 font-mono text-xl">{totalSeen}</p>
</div>
<div className="rounded-md border border-white/10 bg-black/20 p-3">
<p className="text-[11px] uppercase text-white/38">relays</p>
<p className="mt-1 font-mono text-xl">{readRelays.length}</p>
</div>
<div className="rounded-md border border-white/10 bg-black/20 p-3">
<p className="text-[11px] uppercase text-white/38">last</p>
<p className="mt-1 truncate font-mono text-xl">{formatRelativeTime(lastEventAt)}</p>
</div>
</div>
<div className="mt-4 space-y-2">
{events.length > 0 ? (
events.slice(0, 6).map((item) => <LiveEventRow key={item.event.id} item={item} />)
) : (
<div className="rounded-md border border-dashed border-white/15 bg-white/[0.03] p-5 text-sm text-white/50">
Listening for the next event. The relay field will light up as soon as traffic arrives.
</div>
)}
</div>
</section>
);
}
const features = [
{
icon: Gauge,
title: 'Low-friction relay access',
description: 'Point any Nostr client at the relay URL and start reading or publishing through the network.',
},
{
icon: Globe2,
title: 'Built for the public graph',
description: 'Designed around open Nostr primitives, not platform lock-in or private social silos.',
},
{
icon: ShieldCheck,
title: 'Simple by default',
description: 'A focused relay front door with account tools, dashboard paths, and live network visibility.',
},
];
const Index = () => {
const [copied, setCopied] = useState(false);
const { toast } = useToast();
const relayUrl = 'wss://relay.layer.systems';
const { events, status, totalSeen, readRelays, lastEventAt } = useLiveRelayEvents();
useSeoMeta({
title: 'LAYER.systems - Public Nostr Relay',
description: 'A fast, reliable, and open Nostr relay serving the decentralized social network.',
title: 'LAYER.systems - Live Nostr Relay',
description: 'A modern public Nostr relay with live incoming event visibility.',
});
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(relayUrl);
await navigator.clipboard.writeText(RELAY_URL);
setCopied(true);
toast({
title: 'Copied!',
title: 'Copied',
description: 'Relay URL copied to clipboard',
});
setTimeout(() => setCopied(false), 2000);
window.setTimeout(() => setCopied(false), 2000);
} catch {
toast({
title: 'Failed to copy',
description: 'Please copy the URL manually',
description: 'Please copy the relay URL manually',
variant: 'destructive',
});
}
};
const features = [
{
icon: Gift,
title: 'Free',
description: 'No cost to use - accessible for everyone',
},
{
icon: Users,
title: 'Community driven',
description: 'Built and maintained by the Nostr community',
},
{
icon: Globe,
title: 'Global Network',
description: 'Part of the decentralized Nostr ecosystem',
},
{
icon: Server,
title: 'Open Access',
description: 'Free to use for all Nostr clients',
},
];
return (
<div className="min-h-screen bg-gradient-to-br from-background via-background to-muted/20">
{/* Header */}
<header className="absolute top-0 left-0 right-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex justify-end">
<LoginArea className="max-w-60" />
</div>
<div className="min-h-screen bg-[#f4f0e7] text-[#16130e]">
<header className="fixed inset-x-0 top-0 z-50 border-b border-black/10 bg-[#f4f0e7]/80 backdrop-blur-xl">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-3 sm:px-6 lg:px-8">
<Link to="/" className="flex items-center gap-3" aria-label="LAYER.systems home">
<span className="flex h-9 w-9 items-center justify-center rounded-md bg-[#16130e] text-amber-200">
<Server className="h-5 w-5" />
</span>
<span className="font-['Avenir_Next_Condensed','Arial_Narrow',sans-serif] text-xl font-black uppercase">
LAYER.systems
</span>
</Link>
<nav className="hidden items-center gap-1 text-sm font-medium text-black/60 md:flex">
<Button asChild variant="ghost" className="text-black/65 hover:bg-black/5 hover:text-black">
<Link to="/explore">Explore</Link>
</Button>
<Button asChild variant="ghost" className="text-black/65 hover:bg-black/5 hover:text-black">
<Link to="/dashboard">Dashboard</Link>
</Button>
</nav>
<LoginArea className="max-w-60" />
</div>
</header>
{/* Hero Section */}
<div className="relative overflow-hidden">
{/* Animated Background Elements */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-1/4 -left-20 w-96 h-96 bg-primary/5 rounded-full blur-3xl animate-pulse" />
<div className="absolute bottom-1/4 -right-20 w-96 h-96 bg-primary/5 rounded-full blur-3xl animate-pulse delay-1000" />
</div>
<main>
<section className="relative overflow-hidden bg-[#f4f0e7] pt-24">
<div className="absolute inset-0 opacity-[0.28] [background-image:linear-gradient(to_right,#16130e_1px,transparent_1px),linear-gradient(to_bottom,#16130e_1px,transparent_1px)] [background-size:48px_48px]" />
<div className="absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-[#16130e] to-transparent" />
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-16 sm:pt-32 sm:pb-24">
{/* Status Badge */}
{/* <div className="flex justify-center mb-8">
<Badge variant="outline" className="px-4 py-2 text-sm font-medium gap-2 border-primary/20 bg-primary/5">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
Relay Online
</Badge>
</div> */}
<div className="relative mx-auto grid min-h-[calc(100vh-5rem)] max-w-7xl items-center gap-10 px-4 pb-24 sm:px-6 lg:grid-cols-[0.95fr_1.05fr] lg:px-8">
<div className="max-w-2xl">
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-black/10 bg-white/45 px-3 py-1.5 text-sm font-semibold text-black/68 shadow-sm backdrop-blur">
<CircleDot className="h-4 w-4 text-lime-700" />
Public Nostr relay with live event visibility
</div>
{/* Main Heading */}
<div className="text-center space-y-6 mb-12">
<h1 className="text-5xl sm:text-7xl font-bold tracking-tight">
<span className="bg-gradient-to-r from-primary via-primary to-primary/60 bg-clip-text text-transparent">
LAYER.systems
</span>
</h1>
<p className="text-xl sm:text-2xl text-muted-foreground max-w-2xl mx-auto font-light">
Your gateway to the decentralized social network
</p>
<p className="text-base sm:text-lg text-muted-foreground/80 max-w-xl mx-auto">
A fast, reliable, and open Nostr relay connecting you to the future of social media
</p>
</div>
<h1 className="font-['Avenir_Next_Condensed','Arial_Narrow',sans-serif] text-6xl font-black uppercase leading-[0.86] tracking-normal text-[#17130e] sm:text-7xl lg:text-8xl">
Watch the relay breathe.
</h1>
<p className="mt-7 max-w-xl text-lg leading-8 text-black/68 sm:text-xl">
LAYER.systems is a clean public gateway into Nostr. Add the relay, publish from your client, and see the network surface in real time.
</p>
{/* Relay URL Card */}
<div className="max-w-2xl mx-auto mb-16">
<Card className="border-2 border-primary/20 shadow-2xl shadow-primary/5 backdrop-blur-sm bg-card/95">
<CardContent className="p-8">
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="sm:flex-1 min-w-0">
<p className="text-sm text-muted-foreground mb-2">Relay URL</p>
<code className="text-lg sm:text-xl font-mono text-primary break-all">
{relayUrl}
</code>
</div>
<Button
size="lg"
onClick={copyToClipboard}
className="w-full sm:w-auto sm:shrink-0 gap-2 hover:scale-105 transition-transform"
>
{copied ? (
<>
<CheckCircle2 className="w-5 h-5" />
Copied
</>
) : (
<>
<Copy className="w-5 h-5" />
Copy
</>
)}
</Button>
<div className="mt-8 rounded-md border border-black/12 bg-white/56 p-3 shadow-[0_20px_60px_rgba(22,19,14,0.12)] backdrop-blur">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<div className="min-w-0 flex-1 rounded-sm bg-[#16130e] px-4 py-3">
<p className="text-xs uppercase text-white/38">relay url</p>
<code className="mt-1 block break-all font-mono text-base text-amber-100 sm:text-lg">{RELAY_URL}</code>
</div>
<p className="text-sm text-muted-foreground">
Add this URL to your Nostr client to connect to LAYER.systems
</p>
<Button
size="lg"
onClick={copyToClipboard}
className="h-14 shrink-0 bg-[#f6b32d] px-5 text-black hover:bg-[#f2c75d]"
>
{copied ? <CheckCircle2 className="h-5 w-5" /> : <Copy className="h-5 w-5" />}
{copied ? 'Copied' : 'Copy'}
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
{/* Features Grid */}
<div className="max-w-5xl mx-auto">
<h2 className="text-3xl font-bold text-center mb-12">Why Choose LAYER.systems?</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{features.map((feature, index) => (
<Card
key={index}
className="group hover:border-primary/40 transition-all duration-300 hover:shadow-lg hover:shadow-primary/5 hover:-translate-y-1"
>
<CardContent className="p-6 space-y-4">
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<feature.icon className="w-6 h-6 text-primary" />
</div>
<div>
<h3 className="font-semibold text-lg mb-2">{feature.title}</h3>
<p className="text-sm text-muted-foreground">{feature.description}</p>
</div>
</CardContent>
</Card>
<div className="mt-8 grid max-w-xl grid-cols-3 gap-3">
<div className="border-l border-black/15 pl-4">
<p className="font-mono text-2xl">{readRelays.length}</p>
<p className="mt-1 text-sm text-black/50">read relay{readRelays.length === 1 ? '' : 's'}</p>
</div>
<div className="border-l border-black/15 pl-4">
<p className="font-mono text-2xl">{totalSeen}</p>
<p className="mt-1 text-sm text-black/50">events seen</p>
</div>
<div className="border-l border-black/15 pl-4">
<p className="font-mono text-2xl">{formatRelativeTime(lastEventAt)}</p>
<p className="mt-1 text-sm text-black/50">last pulse</p>
</div>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-[1fr_360px]">
<EventConstellation events={events} />
<LiveRelayPanel
events={events}
status={status}
totalSeen={totalSeen}
readRelays={readRelays}
lastEventAt={lastEventAt}
/>
</div>
</div>
</section>
<section className="bg-[#16130e] px-4 py-16 text-white sm:px-6 lg:px-8">
<div className="mx-auto grid max-w-7xl gap-8 lg:grid-cols-[0.72fr_1.28fr]">
<div>
<Badge className="border border-amber-200/25 bg-amber-200/10 text-amber-100 hover:bg-amber-200/10">
<Sparkles className="mr-1 h-3.5 w-3.5" />
relay ready
</Badge>
<h2 className="mt-5 max-w-sm font-['Avenir_Next_Condensed','Arial_Narrow',sans-serif] text-5xl font-black uppercase leading-none">
A quieter front door for a busy network.
</h2>
</div>
<div className="grid gap-3 md:grid-cols-3">
{features.map((feature) => (
<article key={feature.title} className="rounded-md border border-white/10 bg-white/[0.055] p-5">
<feature.icon className="h-6 w-6 text-amber-200" />
<h3 className="mt-5 text-lg font-semibold">{feature.title}</h3>
<p className="mt-3 text-sm leading-6 text-white/58">{feature.description}</p>
</article>
))}
</div>
</div>
</div>
</div>
</section>
{/* How to Connect Section */}
<div className="border-t border-border/40">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
<h2 className="text-3xl font-bold text-center mb-12">Getting Started</h2>
<div className="space-y-8">
<Card>
<CardContent className="p-8">
<div className="space-y-6">
<div className="flex gap-4">
<div className="shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-semibold">
1
</div>
<div>
<h3 className="font-semibold text-lg mb-2">Choose Your Client</h3>
<p className="text-muted-foreground">
Pick a Nostr client like Damus, Amethyst, Snort, or any other compatible application
</p>
</div>
</div>
<div className="flex gap-4">
<div className="shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-semibold">
2
</div>
<div>
<h3 className="font-semibold text-lg mb-2">Add the Relay</h3>
<p className="text-muted-foreground">
In your client settings, add <code className="px-2 py-1 bg-muted rounded text-sm font-mono">{relayUrl}</code> to your relay list
</p>
</div>
</div>
<div className="flex gap-4">
<div className="shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-semibold">
3
</div>
<div>
<h3 className="font-semibold text-lg mb-2">Start Connecting</h3>
<p className="text-muted-foreground">
You're all set! Start posting, following, and connecting with the Nostr network
</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
{/* Footer */}
<footer className="border-t border-border/40 bg-muted/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex flex-col sm:flex-row justify-between items-center gap-4">
<p className="text-sm text-muted-foreground">
© {new Date().getFullYear()} LAYER.systems. Powered by Nostr.
</p>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Server className="w-4 h-4" />
<span>Open and free for all</span>
</div>
<div className="flex items-center gap-4 text-sm">
<a href="/terms" className="text-muted-foreground hover:text-foreground transition-colors">
Terms
</a>
<a href="/privacy" className="text-muted-foreground hover:text-foreground transition-colors">
Privacy
</a>
</div>
<section className="bg-[#ece6d9] px-4 py-14 sm:px-6 lg:px-8">
<div className="mx-auto flex max-w-7xl flex-col gap-6 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-sm font-semibold uppercase text-black/45">next step</p>
<h2 className="mt-2 text-3xl font-bold text-[#16130e]">Explore the public feed or inspect relay activity.</h2>
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<Button asChild size="lg" className="bg-[#16130e] text-white hover:bg-black">
<Link to="/explore">
Open Explore
<ArrowUpRight className="h-4 w-4" />
</Link>
</Button>
<Button asChild size="lg" variant="outline" className="border-black/20 bg-transparent hover:bg-black/5">
<Link to="/dashboard">
Relay Dashboard
<Zap className="h-4 w-4" />
</Link>
</Button>
</div>
</div>
</section>
</main>
<footer className="border-t border-black/10 bg-[#ece6d9] px-4 py-8 sm:px-6 lg:px-8">
<div className="mx-auto flex max-w-7xl flex-col gap-4 text-sm text-black/55 sm:flex-row sm:items-center sm:justify-between">
<p>{new Date().getFullYear()} LAYER.systems. Powered by Nostr.</p>
<div className="flex gap-5">
<Link to="/terms" className="hover:text-black">Terms</Link>
<Link to="/privacy" className="hover:text-black">Privacy</Link>
</div>
</div>
</footer>