Refresh dashboard styling with bold retro cards

This commit is contained in:
2026-06-18 10:12:14 +02:00
parent 4acdb31b8a
commit cf68f1d7f5
12 changed files with 761 additions and 333 deletions

325
DESIGN.md Normal file
View File

@@ -0,0 +1,325 @@
# LAYER.systems Design Direction
## 1. Design Intent
LAYER.systems should feel like Nostr relay infrastructure made visible: direct, fast, public, and protocol-native. The visual direction takes cues from bold editorial community sites such as `einundzwanzig.space`: strong navigation, large type, ticker energy, clear content bands, and a public-broadcast feeling.
This is not a clone. LAYER.systems needs its own identity: orange, infrastructural, decentralized, and practical. The site should make the relay endpoint the center of the experience, not a footnote.
## 2. Reference Translation
Inspired by `einundzwanzig.space`:
- Strong top navigation with grouped links.
- Large typographic hero with direct messaging.
- Horizontal ticker / marquee energy for relay URLs, protocol phrases, and network language.
- Clear content bands for relay status, usage, and connection steps.
- Editorial confidence instead of generic landing-page polish.
- Playful tone without losing operational credibility.
Translate for LAYER.systems:
- Use an orange-led palette instead of purple.
- Replace podcast/news hierarchy with relay endpoint and Nostr infrastructure hierarchy.
- Replace sponsor/content blocks with relay, protocol, client, and network modules.
- Use `wss://relay.layer.systems` as a visible recurring design motif.
- Make the copyable relay URL feel like the main product surface.
## 3. Personality
The interface should be:
- Public, not gated.
- Bold, not glossy.
- Operational, not corporate.
- Protocol-aware, not protocol-obscure.
- Human in tone, calm in structure.
Avoid:
- Generic orange gradient SaaS aesthetics.
- Overly soft pastel community branding.
- Crypto-bro visual language.
- Dense technical jargon in primary UI copy.
- Stock-photo hero sections.
## 4. Visual System
### Color Palette
Use orange as the identity anchor, but avoid a one-note all-orange interface.
Suggested palette:
- `Signal Orange`: `#F97316` for primary actions and identity marks.
- `Deep Ink`: `#18110D` for dark text, footer, and hard borders.
- `Relay Amber`: `#FACC15` for secondary highlights and ticker details.
- `Paper`: `#FFF7E8` for warm page background.
- `Chalk`: `#FFFFFF` for content surfaces.
- `Clay`: `#7C3F1D` for subdued labels and supporting text.
- `Protocol Green`: `#17A673` for live/verified states.
- `Network Blue`: `#2563EB` as a sparing cool accent.
Usage rules:
- Orange should lead, not flood.
- Use ink-heavy contrast and crisp borders for readability.
- Use amber, green, and blue sparingly to prevent the interface from becoming monochrome.
- Relay cards should remain legible before they are decorative.
### Typography
The type system should feel editorial and independent.
Recommended direction:
- Display: bold grotesk weight for hero and section titles.
- Body: readable sans with warmth and strong numerals.
- Mono: restrained monospace for relay URLs, event kinds, public keys, and tags.
Implementation preference:
- Use `@fontsource` packages when available.
- Use CSS variables for font families.
- Keep body text comfortable on content-heavy sections.
- Keep hero type large and compact, but never allow it to overflow mobile widths.
## 5. Layout
### Page Structure
The first version should be a single-page relay experience:
1. Sticky navigation
2. Relay ticker
3. Hero / identity
4. Copyable relay endpoint
5. Relay feature modules
6. Client connection steps
7. Footer with terms, privacy, and dashboard links
### Navigation
The nav should feel practical and slightly editorial:
- Left: LAYER.systems wordmark.
- Center: Relay, Signal, Connect, Explore.
- Right: `LoginArea`.
- Mobile: compact header with persistent login/account access.
Use strong text links and clear hover/focus treatment. Avoid oversized pill buttons for every nav item.
### Ticker
Add a horizontal ticker near the top of the page.
Potential ticker phrases:
- `LAYER.systems`
- `public Nostr relay`
- `wss://relay.layer.systems`
- `open protocols`
- `relay-first social`
- `NIP-aware infrastructure`
- `bring your own client`
- `events over platforms`
Ticker behavior:
- Continuous motion on desktop.
- Static fallback with `prefers-reduced-motion`.
- High contrast.
- No essential information should exist only in the ticker.
### Hero
The hero should be typographic and signal-heavy.
Content:
- Main title: `Relay signal for the open social web.`
- Supporting line: a concise statement about public Nostr relay infrastructure.
- Primary CTA: jump to the relay URL.
- Secondary CTA: jump to connection steps.
Visual treatment:
- Large editorial headline.
- Copyable endpoint module.
- Subtle grid or protocol texture is acceptable if it does not reduce readability.
- Use real relay/data motifs rather than abstract decorative blobs.
## 6. Components
### Relay Endpoint Card
Required elements:
- Relay URL.
- Copy button with copied state.
- Access mode.
- Network/protocol labels.
- Clear read/write language when applicable.
Design:
- Rectangular panel with `8px` or less radius.
- Crisp border and offset shadow.
- Relay URL must wrap safely on small screens.
- Copy action must remain reachable on mobile.
### Feature Cards
Required elements:
- Icon.
- Short title.
- One practical sentence.
Design:
- Rectangular cards.
- Crisp border.
- Subtle hover shift.
- No nested cards.
### Connection Steps
Required elements:
- Step number.
- Client action.
- Relay URL in the add-relay step.
Design:
- Structured rows or blocks.
- Step numbers should be visually strong.
- Code text should wrap cleanly.
### Empty States
If live relay status or event lists are added later, empty states should be minimalist and practical:
- Explain what data will appear.
- Include a short hint about adding `wss://relay.layer.systems` from any Nostr client.
- Avoid speculative promises about uptime or persistence.
## 7. Motion
Motion should feel like signal movement, not decoration.
Use:
- Ticker movement.
- Subtle hover shifts on cards and CTAs.
- Focus-visible rings with a quick transition.
Avoid:
- Heavy parallax.
- Decorative floating orbs.
- Motion that distracts from reading the relay URL.
Respect `prefers-reduced-motion`.
## 8. Imagery and Texture
This site does not need stock photography.
Preferred visual motifs:
- Relay/network linework.
- Terminal-inspired grid texture.
- Endpoint/code fragments.
- Protocol tags and event-kind labels.
- Simple infrastructure diagrams.
Texture direction:
- Light paper/noise texture is acceptable.
- Grid backgrounds are acceptable.
- Keep texture subtle enough that body text remains AA compliant.
## 9. Accessibility
Requirements:
- WCAG 2.1 AA contrast for text and controls.
- Keyboard-accessible navigation and controls.
- Visible focus states.
- Semantic headings.
- Descriptive alt text for static images.
- Empty `alt` for purely decorative images.
- No unsafe HTML injection.
## 10. Responsive Behavior
Mobile:
- Single-column flow.
- Sticky compact nav.
- Ticker can become static.
- Relay URL must wrap without layout overflow.
- Hero type must scale by breakpoint, not viewport-width formulas.
Tablet:
- Two-column section layouts where useful.
- Endpoint card remains prominent.
Desktop:
- Editorial grid with asymmetric section rhythm.
- Relay endpoint and hero copy share the first viewport.
- Keep primary content width controlled for readability.
## 11. Copy Tone
Tone should be direct and protocol-native.
Good:
- `Relay signal for the open social web.`
- `A public Nostr relay for portable social identity.`
- `Add the relay URL from any Nostr client.`
- `Events over platforms.`
Avoid:
- `Revolutionizing social engagement`
- `The ultimate decentralized ecosystem`
- `Next-generation community platform`
- `Seamless innovative experiences`
## 12. Implementation Notes
- Use existing shadcn/ui primitives.
- Keep cards at `8px` radius or less unless a local component requires otherwise.
- Use `cn()` for class composition when class branching is needed.
- Use lucide icons where icons are needed.
- Define color and font tokens in CSS variables.
- Keep sections as full-width bands with constrained inner content.
- Do not nest UI cards inside UI cards.
- Do not add unsafe HTML rendering for Nostr content.
- Sanitize all event-sourced URLs before rendering.
## 13. Documentation Tie-In
The `/docs` folder can document the design system once implementation expands.
Recommended docs:
- `/docs/design-system.md`: colors, typography, spacing, components.
- `/docs/content-guidelines.md`: tone, protocol language, relay usage guidance.
- `/docs/nostr-data.md`: relay-facing event kinds, tags, validation, and query patterns.
## 14. Design Acceptance Criteria
- The site clearly reads as LAYER.systems in the first viewport.
- The visual direction feels inspired by `einundzwanzig.space` but has its own orange Nostr relay identity.
- The relay endpoint is visually central, not secondary.
- The interface remains readable and usable at 360px width.
- Motion respects `prefers-reduced-motion`.
- UI contrast meets WCAG 2.1 AA.
- The design avoids generic orange SaaS styling.

View File

@@ -14,7 +14,7 @@ export function DashboardStats({ pubkey }: DashboardStatsProps) {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{[...Array(4)].map((_, i) => (
<Card key={i}>
<Card key={i} className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-4 rounded" />
@@ -41,39 +41,39 @@ export function DashboardStats({ pubkey }: DashboardStatsProps) {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Loaded Events</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-black uppercase">Loaded Events</CardTitle>
<FileText className="h-4 w-4 text-primary" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalEvents}</div>
<div className="font-mono text-3xl font-black">{stats.totalEvents}</div>
<p className="text-xs text-muted-foreground">
All events loaded
</p>
</CardContent>
</Card>
<Card>
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Event Types</CardTitle>
<Hash className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-black uppercase">Event Types</CardTitle>
<Hash className="h-4 w-4 text-primary" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{uniqueKinds}</div>
<div className="font-mono text-3xl font-black">{uniqueKinds}</div>
<p className="text-xs text-muted-foreground">
Different kinds used
</p>
</CardContent>
</Card>
<Card>
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Most Used</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-black uppercase">Most Used</CardTitle>
<Activity className="h-4 w-4 text-primary" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
<div className="font-mono text-3xl font-black">
{Object.entries(stats.eventsByKind).length > 0
? `Kind ${
Object.entries(stats.eventsByKind).sort(
@@ -94,13 +94,13 @@ export function DashboardStats({ pubkey }: DashboardStatsProps) {
</CardContent>
</Card>
<Card>
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Last Activity</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-black uppercase">Last Activity</CardTitle>
<Clock className="h-4 w-4 text-primary" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
<div className="font-mono text-3xl font-black">
{stats.lastActivity
? new Date(stats.lastActivity * 1000).toLocaleDateString(
undefined,

View File

@@ -122,7 +122,7 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
if (isLoading) {
return (
<Card>
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader>
<Skeleton className="h-6 w-40 mb-2" />
<Skeleton className="h-4 w-64" />
@@ -146,9 +146,9 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
if (isError || !stats) {
return (
<Card className="border-dashed">
<Card className="rounded-none border-2 border-dashed border-foreground bg-card shadow-none">
<CardHeader>
<CardTitle>Your Events</CardTitle>
<CardTitle className="font-black uppercase">Your Events</CardTitle>
<CardDescription>
Explore your published events and manage deletion requests.
</CardDescription>
@@ -164,9 +164,9 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
if (stats.events.length === 0) {
return (
<Card className="border-dashed">
<Card className="rounded-none border-2 border-dashed border-foreground bg-card shadow-none">
<CardHeader>
<CardTitle>Your Events</CardTitle>
<CardTitle className="font-black uppercase">Your Events</CardTitle>
<CardDescription>
Explore your published events and manage deletion requests.
</CardDescription>
@@ -185,10 +185,10 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
return (
<>
<Card className="overflow-hidden">
<Card className="overflow-hidden rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0">
<CardTitle className="break-words">Your Events</CardTitle>
<CardTitle className="break-words font-black uppercase">Your Events</CardTitle>
<CardDescription className="break-words">
Browse your published events and request deletion directly from your dashboard.
</CardDescription>
@@ -197,7 +197,7 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
className="pl-8"
className="rounded-none border-2 border-foreground pl-8"
placeholder="Search content..."
value={search}
onChange={(event) => {
@@ -221,11 +221,11 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
return (
<div
key={event.id}
className="flex flex-col gap-2 rounded-lg border bg-card/40 p-3 transition hover:bg-accent/40 sm:flex-row sm:items-center sm:justify-between"
className="flex flex-col gap-2 border-2 border-foreground bg-card p-3 transition hover:-translate-y-0.5 hover:bg-secondary sm:flex-row sm:items-center sm:justify-between"
>
<div className="space-y-1 min-w-0 overflow-hidden">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary" className="text-xs shrink-0">
<Badge variant="secondary" className="shrink-0 rounded-none border border-foreground text-xs font-black uppercase">
{label}
</Badge>
<span className="text-xs text-muted-foreground shrink-0">
@@ -252,7 +252,7 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
<Button
variant="outline"
size="sm"
className="whitespace-nowrap"
className="whitespace-nowrap rounded-none border-2 border-foreground font-bold"
onClick={() => handleOpenDialog(event)}
>
<Trash2 className="mr-2 h-4 w-4" />
@@ -268,9 +268,9 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-3 py-2 text-sm">
<div className="rounded-md bg-muted px-3 py-2">
<div className="border-2 border-foreground bg-muted px-3 py-2">
<div className="flex items-center justify-between mb-1">
<Badge variant="secondary" className="text-2xs">
<Badge variant="secondary" className="rounded-none border border-foreground text-2xs">
{label}
</Badge>
<span className="text-xs text-muted-foreground">
@@ -288,7 +288,7 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
<Textarea
value={reason}
onChange={(event) => setReason(event.target.value)}
className="min-h-[80px] text-sm"
className="min-h-[80px] rounded-none border-2 border-foreground text-sm"
placeholder="Explain why you are requesting this deletion."
/>
</div>
@@ -337,6 +337,7 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
<Button
variant="outline"
size="sm"
className="rounded-none border-2 border-foreground font-bold"
disabled={pageSafe <= 1}
onClick={() => setPage((current) => Math.max(current - 1, 1))}
>
@@ -354,6 +355,7 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
<Button
variant="outline"
size="sm"
className="rounded-none border-2 border-foreground font-bold"
disabled={pageSafe >= totalPages}
onClick={() =>
setPage((current) => Math.min(current + 1, totalPages))

View File

@@ -109,7 +109,7 @@ export function EventKindsChart() {
if (isLoading) {
return (
<Card className="flex flex-col">
<Card className="flex flex-col rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader className="items-center pb-0">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-4 w-48 mt-2" />
@@ -129,7 +129,7 @@ export function EventKindsChart() {
if (!user) {
return (
<Card className="flex flex-col">
<Card className="flex flex-col rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader className="items-center pb-0">
<CardTitle>Event Kinds Distribution</CardTitle>
<CardDescription>Log in to see your event distribution</CardDescription>
@@ -143,7 +143,7 @@ export function EventKindsChart() {
if (!stats || chartData.length === 0) {
return (
<Card className="flex flex-col">
<Card className="flex flex-col rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader className="items-center pb-0">
<CardTitle>Event Kinds Distribution</CardTitle>
<CardDescription>Your published event types</CardDescription>
@@ -156,9 +156,9 @@ export function EventKindsChart() {
}
return (
<Card className="flex flex-col overflow-hidden">
<Card className="flex flex-col overflow-hidden rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader className="items-center pb-0">
<CardTitle>Event Kinds Distribution</CardTitle>
<CardTitle className="font-black uppercase">Event Kinds Distribution</CardTitle>
<CardDescription>Your top 5 event types</CardDescription>
</CardHeader>
<CardContent className="flex-1 pb-0">

View File

@@ -88,7 +88,7 @@ export function RecentActivityChart() {
if (isLoading) {
return (
<Card>
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader>
<Skeleton className="h-6 w-40 mb-2" />
<Skeleton className="h-4 w-60" />
@@ -102,7 +102,7 @@ export function RecentActivityChart() {
if (!user) {
return (
<Card>
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>Last 30 days</CardDescription>
@@ -118,7 +118,7 @@ export function RecentActivityChart() {
if (chartData.length === 0) {
return (
<Card>
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>Last 30 days</CardDescription>
@@ -133,9 +133,9 @@ export function RecentActivityChart() {
}
return (
<Card className="overflow-hidden">
<Card className="overflow-hidden rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardTitle className="font-black uppercase">Recent Activity</CardTitle>
<CardDescription>
Your event activity over the last 30 days
</CardDescription>

View File

@@ -27,7 +27,7 @@ export function RecentActivityList({ pubkey }: RecentActivityListProps) {
if (isLoading) {
return (
<Card>
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader>
<Skeleton className="h-6 w-32 mb-2" />
<Skeleton className="h-4 w-48" />
@@ -51,9 +51,9 @@ export function RecentActivityList({ pubkey }: RecentActivityListProps) {
if (!stats || stats.recentEvents.length === 0) {
return (
<Card>
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardTitle className="font-black uppercase">Recent Activity</CardTitle>
<CardDescription>Your latest events</CardDescription>
</CardHeader>
<CardContent>
@@ -66,9 +66,9 @@ export function RecentActivityList({ pubkey }: RecentActivityListProps) {
}
return (
<Card>
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardTitle className="font-black uppercase">Recent Activity</CardTitle>
<CardDescription>Your latest {stats.recentEvents.length} events</CardDescription>
</CardHeader>
<CardContent>
@@ -91,7 +91,7 @@ export function RecentActivityList({ pubkey }: RecentActivityListProps) {
>
<div className="space-y-1 flex-1 min-w-0 overflow-hidden">
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="secondary" className="text-xs shrink-0">
<Badge variant="secondary" className="shrink-0 rounded-none border border-foreground text-xs font-black uppercase">
{kindName}
</Badge>
<span className="text-xs text-muted-foreground shrink-0">

View File

@@ -1,4 +1,4 @@
import { Home, LayoutDashboard, FileText, Download } from 'lucide-react';
import { Download, FileText, Home, LayoutDashboard, Radio } from 'lucide-react';
import { Link, useLocation } from 'react-router-dom';
import {
Sidebar,
@@ -40,17 +40,28 @@ export function AppSidebar() {
const location = useLocation();
return (
<Sidebar>
<Sidebar className="border-r-2 border-foreground">
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<div className="m-2 mb-4 border-2 border-foreground bg-primary p-3 text-primary-foreground shadow-[4px_4px_0_hsl(var(--foreground))]">
<div className="flex items-center gap-2 font-black uppercase leading-none">
<Radio className="h-5 w-5" />
LAYER.systems
</div>
<p className="mt-2 text-xs font-bold uppercase">Relay dashboard</p>
</div>
<SidebarGroupLabel className="font-black uppercase text-foreground">Navigation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{navigationItems.map((item) => {
const isActive = location.pathname === item.url;
return (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild isActive={isActive}>
<SidebarMenuButton
asChild
isActive={isActive}
className="rounded-none font-bold data-[active=true]:border-2 data-[active=true]:border-foreground data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
>
<Link to={item.url}>
<item.icon />
<span>{item.title}</span>

View File

@@ -1,42 +1,44 @@
@import "@fontsource-variable/inter";
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 14.9020%;
--background: 42 100% 97%;
--foreground: 20 23% 8%;
--card: 0 0% 100%;
--card-foreground: 0 0% 14.9020%;
--card-foreground: 20 23% 8%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 14.9020%;
--primary: 37.6923 92.1260% 50.1961%;
--primary-foreground: 0 0% 0%;
--secondary: 220.0000 14.2857% 95.8824%;
--secondary-foreground: 215 13.7931% 34.1176%;
--muted: 210 20.0000% 98.0392%;
--muted-foreground: 220 8.9362% 46.0784%;
--accent: 48.0000 100.0000% 96.0784%;
--accent-foreground: 22.7273 82.5000% 31.3725%;
--popover-foreground: 20 23% 8%;
--primary: 28 96% 52%;
--primary-foreground: 20 23% 8%;
--secondary: 39 100% 90%;
--secondary-foreground: 20 23% 8%;
--muted: 38 42% 91%;
--muted-foreground: 24 12% 35%;
--accent: 49 95% 58%;
--accent-foreground: 20 23% 8%;
--destructive: 0 84.2365% 60.1961%;
--destructive-foreground: 0 0% 100%;
--border: 220 13.0435% 90.9804%;
--input: 220 13.0435% 90.9804%;
--ring: 37.6923 92.1260% 50.1961%;
--chart-1: 37.6923 92.1260% 50.1961%;
--chart-2: 32.1327 94.6188% 43.7255%;
--chart-3: 25.9649 90.4762% 37.0588%;
--chart-4: 22.7273 82.5000% 31.3725%;
--chart-5: 21.7143 77.7778% 26.4706%;
--sidebar: 210 20.0000% 98.0392%;
--sidebar-foreground: 0 0% 14.9020%;
--sidebar-primary: 37.6923 92.1260% 50.1961%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 48.0000 100.0000% 96.0784%;
--sidebar-accent-foreground: 22.7273 82.5000% 31.3725%;
--sidebar-border: 220 13.0435% 90.9804%;
--sidebar-ring: 37.6923 92.1260% 50.1961%;
--font-sans: Inter, sans-serif;
--border: 25 31% 82%;
--input: 25 31% 82%;
--ring: 28 96% 52%;
--chart-1: 28 96% 52%;
--chart-2: 49 95% 58%;
--chart-3: 159 65% 36%;
--chart-4: 210 68% 42%;
--chart-5: 15 80% 42%;
--sidebar: 42 100% 97%;
--sidebar-foreground: 20 23% 8%;
--sidebar-primary: 28 96% 52%;
--sidebar-primary-foreground: 20 23% 8%;
--sidebar-accent: 49 95% 58%;
--sidebar-accent-foreground: 20 23% 8%;
--sidebar-border: 25 31% 82%;
--sidebar-ring: 28 96% 52%;
--font-sans: "Inter Variable", Inter, sans-serif;
--font-serif: Source Serif 4, serif;
--font-mono: JetBrains Mono, monospace;
--radius: 0.375rem;
@@ -59,39 +61,39 @@
}
.dark {
--background: 0 0% 9.0196%;
--foreground: 0 0% 89.8039%;
--card: 0 0% 14.9020%;
--card-foreground: 0 0% 89.8039%;
--popover: 0 0% 14.9020%;
--popover-foreground: 0 0% 89.8039%;
--primary: 37.6923 92.1260% 50.1961%;
--primary-foreground: 0 0% 0%;
--secondary: 0 0% 14.9020%;
--secondary-foreground: 0 0% 89.8039%;
--muted: 0 0% 12.1569%;
--muted-foreground: 0 0% 63.9216%;
--accent: 22.7273 82.5000% 31.3725%;
--accent-foreground: 48 96.6387% 76.6667%;
--background: 20 23% 8%;
--foreground: 42 100% 95%;
--card: 20 18% 12%;
--card-foreground: 42 100% 95%;
--popover: 20 18% 12%;
--popover-foreground: 42 100% 95%;
--primary: 31 100% 57%;
--primary-foreground: 20 23% 8%;
--secondary: 22 18% 17%;
--secondary-foreground: 42 100% 95%;
--muted: 23 15% 19%;
--muted-foreground: 37 24% 72%;
--accent: 49 95% 58%;
--accent-foreground: 20 23% 8%;
--destructive: 0 84.2365% 60.1961%;
--destructive-foreground: 0 0% 100%;
--border: 0 0% 25.0980%;
--input: 0 0% 25.0980%;
--ring: 37.6923 92.1260% 50.1961%;
--chart-1: 43.2558 96.4126% 56.2745%;
--chart-2: 32.1327 94.6188% 43.7255%;
--chart-3: 22.7273 82.5000% 31.3725%;
--chart-4: 25.9649 90.4762% 37.0588%;
--chart-5: 22.7273 82.5000% 31.3725%;
--sidebar: 0 0% 5.8824%;
--sidebar-foreground: 0 0% 89.8039%;
--sidebar-primary: 37.6923 92.1260% 50.1961%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 22.7273 82.5000% 31.3725%;
--sidebar-accent-foreground: 48 96.6387% 76.6667%;
--sidebar-border: 0 0% 25.0980%;
--sidebar-ring: 37.6923 92.1260% 50.1961%;
--font-sans: Inter, sans-serif;
--border: 24 18% 28%;
--input: 24 18% 28%;
--ring: 31 100% 57%;
--chart-1: 31 100% 57%;
--chart-2: 49 95% 58%;
--chart-3: 159 65% 46%;
--chart-4: 210 68% 58%;
--chart-5: 15 80% 52%;
--sidebar: 20 23% 8%;
--sidebar-foreground: 42 100% 95%;
--sidebar-primary: 31 100% 57%;
--sidebar-primary-foreground: 20 23% 8%;
--sidebar-accent: 22 18% 17%;
--sidebar-accent-foreground: 42 100% 95%;
--sidebar-border: 24 18% 28%;
--sidebar-ring: 31 100% 57%;
--font-sans: "Inter Variable", Inter, sans-serif;
--font-serif: Source Serif 4, serif;
--font-mono: JetBrains Mono, monospace;
--radius: 0.375rem;
@@ -119,5 +121,37 @@
body {
@apply bg-background text-foreground;
font-family: var(--font-sans);
letter-spacing: var(--tracking-normal);
}
}
html {
scroll-behavior: smooth;
}
}
@layer utilities {
.relay-ticker-track {
animation: relay-marquee 30s linear infinite;
}
}
@keyframes relay-marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(-100%);
}
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
.relay-ticker-track {
animation: none;
}
}

View File

@@ -7,7 +7,7 @@ import { RecentActivityList } from '@/components/dashboard/RecentActivityList';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { Card, CardContent } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { InfoIcon } from 'lucide-react';
import { Activity, InfoIcon } from 'lucide-react';
import { Account, useLoggedInAccounts } from '@/hooks/useLoggedInAccounts';
import { genUserName } from '@/lib/genUserName';
@@ -21,17 +21,17 @@ export function Dashboard() {
return (
<SidebarProvider>
<div className="flex min-h-screen w-full overflow-x-hidden">
<div className="flex min-h-screen w-full overflow-x-hidden bg-background">
<AppSidebar />
<main className="flex-1 min-w-0">
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background px-4 lg:h-[60px] lg:px-6">
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b-2 border-foreground bg-background/95 px-4 backdrop-blur lg:h-[60px] lg:px-6">
<SidebarTrigger />
<h1 className="text-lg font-semibold md:text-xl truncate">Dashboard</h1>
<h1 className="truncate text-lg font-black uppercase md:text-xl">Dashboard</h1>
</div>
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
<div className="flex-1 space-y-6 overflow-x-hidden bg-[linear-gradient(90deg,hsl(var(--border))_1px,transparent_1px),linear-gradient(180deg,hsl(var(--border))_1px,transparent_1px)] bg-[size:32px_32px] p-4 md:p-6 lg:p-8">
{!user ? (
<Card className="border-dashed">
<Card className="rounded-none border-2 border-dashed border-foreground bg-card shadow-[6px_6px_0_hsl(var(--primary))]">
<CardContent className="py-12 px-8 text-center">
<div className="max-w-sm mx-auto space-y-4">
<Alert>
@@ -45,12 +45,16 @@ export function Dashboard() {
</Card>
) : (
<>
<div className="space-y-2">
<h2 className="text-2xl md:text-3xl font-bold tracking-tight break-words">
<div className="border-2 border-foreground bg-card p-5 shadow-[6px_6px_0_hsl(var(--primary))]">
<div className="mb-4 inline-flex items-center gap-2 border-2 border-foreground bg-primary px-3 py-1 text-xs font-black uppercase text-primary-foreground">
<Activity className="h-3.5 w-3.5" />
Account signal
</div>
<h2 className="break-words text-3xl font-black leading-none md:text-5xl">
Welcome back {currentUser ? getDisplayName(currentUser) : ''}!
</h2>
<p className="text-sm md:text-base text-muted-foreground">
Here's an overview of your Nostr activity and statistics.
<p className="mt-3 max-w-2xl text-sm font-medium leading-7 text-muted-foreground md:text-base">
Your Nostr activity, event kinds, and recent relay-facing signal in one place.
</p>
</div>

View File

@@ -11,17 +11,17 @@ export function DashboardEvents() {
return (
<SidebarProvider>
<div className="flex min-h-screen w-full overflow-x-hidden">
<div className="flex min-h-screen w-full overflow-x-hidden bg-background">
<AppSidebar />
<main className="flex-1 min-w-0">
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background px-4 lg:h-[60px] lg:px-6">
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b-2 border-foreground bg-background/95 px-4 backdrop-blur lg:h-[60px] lg:px-6">
<SidebarTrigger />
<h1 className="text-lg font-semibold md:text-xl truncate">My Events</h1>
<h1 className="truncate text-lg font-black uppercase md:text-xl">My Events</h1>
</div>
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
<div className="flex-1 space-y-6 overflow-x-hidden bg-[linear-gradient(90deg,hsl(var(--border))_1px,transparent_1px),linear-gradient(180deg,hsl(var(--border))_1px,transparent_1px)] bg-[size:32px_32px] p-4 md:p-6 lg:p-8">
{!user ? (
<Card className="border-dashed">
<Card className="rounded-none border-2 border-dashed border-foreground bg-card shadow-[6px_6px_0_hsl(var(--primary))]">
<CardContent className="py-12 px-8 text-center">
<div className="max-w-sm mx-auto space-y-4">
<Alert>
@@ -35,11 +35,14 @@ export function DashboardEvents() {
</Card>
) : (
<div className="space-y-4">
<div className="space-y-2">
<h2 className="text-2xl md:text-3xl font-bold tracking-tight break-words">
<div className="border-2 border-foreground bg-card p-5 shadow-[6px_6px_0_hsl(var(--primary))]">
<div className="mb-4 inline-flex border-2 border-foreground bg-primary px-3 py-1 text-xs font-black uppercase text-primary-foreground">
Event explorer
</div>
<h2 className="break-words text-3xl font-black leading-none md:text-5xl">
Your Nostr events
</h2>
<p className="text-sm md:text-base text-muted-foreground max-w-2xl">
<p className="mt-3 max-w-2xl text-sm font-medium leading-7 text-muted-foreground md:text-base">
Browse all events you have published on Nostr, search through their content,
and publish deletion requests when you want something removed.
</p>

View File

@@ -89,17 +89,17 @@ export function DashboardExport() {
return (
<SidebarProvider>
<div className="flex min-h-screen w-full overflow-x-hidden">
<div className="flex min-h-screen w-full overflow-x-hidden bg-background">
<AppSidebar />
<main className="flex-1 min-w-0">
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background px-4 lg:h-[60px] lg:px-6">
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b-2 border-foreground bg-background/95 px-4 backdrop-blur lg:h-[60px] lg:px-6">
<SidebarTrigger />
<h1 className="text-lg font-semibold md:text-xl truncate">Export Following List</h1>
<h1 className="truncate text-lg font-black uppercase md:text-xl">Export Following List</h1>
</div>
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
<div className="flex-1 space-y-6 overflow-x-hidden bg-[linear-gradient(90deg,hsl(var(--border))_1px,transparent_1px),linear-gradient(180deg,hsl(var(--border))_1px,transparent_1px)] bg-[size:32px_32px] p-4 md:p-6 lg:p-8">
{!user ? (
<Card className="border-dashed">
<Card className="rounded-none border-2 border-dashed border-foreground bg-card shadow-[6px_6px_0_hsl(var(--primary))]">
<CardContent className="py-12 px-8 text-center">
<div className="max-w-sm mx-auto space-y-4">
<Alert>
@@ -112,10 +112,13 @@ export function DashboardExport() {
</CardContent>
</Card>
) : (
<div className="max-w-2xl mx-auto space-y-6">
<Card>
<div className="mx-auto max-w-3xl space-y-6">
<Card className="rounded-none border-2 border-foreground bg-card shadow-[6px_6px_0_hsl(var(--primary))]">
<CardHeader>
<CardTitle>Backup Your Following List</CardTitle>
<div className="mb-2 inline-flex w-fit border-2 border-foreground bg-primary px-3 py-1 text-xs font-black uppercase text-primary-foreground">
Kind 3 backup
</div>
<CardTitle className="text-3xl font-black leading-none">Backup your following list</CardTitle>
<CardDescription>
Export your contact list as a JSON file for backup or migration purposes.
</CardDescription>
@@ -148,11 +151,11 @@ export function DashboardExport() {
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<Card className="rounded-none border-2 border-foreground shadow-none">
<CardContent className="pt-6">
<div className="flex items-center gap-4">
<div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
<Users className="h-6 w-6 text-primary" />
<div className="flex h-12 w-12 items-center justify-center border-2 border-foreground bg-primary text-primary-foreground">
<Users className="h-6 w-6 text-primary-foreground" />
</div>
<div>
<p className="text-sm text-muted-foreground">Following</p>
@@ -162,11 +165,11 @@ export function DashboardExport() {
</CardContent>
</Card>
<Card>
<Card className="rounded-none border-2 border-foreground shadow-none">
<CardContent className="pt-6">
<div className="flex items-center gap-4">
<div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
<Calendar className="h-6 w-6 text-primary" />
<div className="flex h-12 w-12 items-center justify-center border-2 border-foreground bg-primary text-primary-foreground">
<Calendar className="h-6 w-6 text-primary-foreground" />
</div>
<div>
<p className="text-sm text-muted-foreground">Last Updated</p>
@@ -194,8 +197,8 @@ export function DashboardExport() {
</div>
<div className="space-y-4">
<div className="rounded-lg border p-4 space-y-2">
<h3 className="font-semibold text-sm">What's included:</h3>
<div className="space-y-2 border-2 border-foreground bg-secondary p-4">
<h3 className="text-sm font-black uppercase">What's included:</h3>
<ul className="text-sm text-muted-foreground space-y-1">
<li>• Complete list of all {followingCount} accounts you follow</li>
<li>• Public keys (pubkeys) for each account</li>
@@ -208,7 +211,7 @@ export function DashboardExport() {
<Button
onClick={handleExport}
disabled={isExporting || !contactListEvent}
className="w-full"
className="w-full rounded-none border-2 border-foreground font-black uppercase"
size="lg"
>
<Download className="mr-2 h-5 w-5" />
@@ -224,9 +227,9 @@ export function DashboardExport() {
</CardContent>
</Card>
<Card>
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
<CardHeader>
<CardTitle className="text-base">About Your Following List</CardTitle>
<CardTitle className="text-base font-black uppercase">About your following list</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground space-y-2">
<p>

View File

@@ -1,15 +1,87 @@
import { useSeoMeta } from '@unhead/react';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import {
Activity,
ArrowDown,
CheckCircle2,
Copy,
Globe2,
Radio,
Server,
ShieldCheck,
Users,
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 { LoginArea } from '@/components/auth/LoginArea';
const relayUrl = 'wss://relay.layer.systems';
const tickerItems = [
'LAYER.systems',
'public Nostr relay',
'wss://relay.layer.systems',
'open protocols',
'relay-first social',
'NIP-aware infrastructure',
'bring your own client',
'events over platforms',
];
const features = [
{
icon: Zap,
title: 'Fast by design',
description: 'A focused relay endpoint for clients that need clean access to the Nostr network.',
},
{
icon: ShieldCheck,
title: 'Open access',
description: 'Free to use, easy to add, and built around portable Nostr identity instead of account lock-in.',
},
{
icon: Globe2,
title: 'Protocol native',
description: 'Publishes and queries standard Nostr events so your client stays interoperable.',
},
{
icon: Users,
title: 'Community useful',
description: 'A practical relay surface for people, apps, and experiments building on decentralized social media.',
},
];
const connectionSteps = [
{
label: '01',
title: 'Open your Nostr client',
description: 'Use Damus, Amethyst, Snort, Coracle, or any client that lets you manage relays.',
},
{
label: '02',
title: 'Add the relay URL',
description: 'Paste the relay endpoint into your read and write relay list.',
},
{
label: '03',
title: 'Publish and query',
description: 'Your client can now send and receive events through LAYER.systems.',
},
];
const relayStats = [
['Endpoint', relayUrl],
['Access', 'public'],
['Network', 'Nostr'],
['Mode', 'read + write'],
];
const Index = () => {
const [copied, setCopied] = useState(false);
const { toast } = useToast();
const relayUrl = 'wss://relay.layer.systems';
useSeoMeta({
title: 'LAYER.systems - Public Nostr Relay',
@@ -21,7 +93,7 @@ const Index = () => {
await navigator.clipboard.writeText(relayUrl);
setCopied(true);
toast({
title: 'Copied!',
title: 'Copied',
description: 'Relay URL copied to clipboard',
});
setTimeout(() => setCopied(false), 2000);
@@ -34,207 +106,181 @@ const Index = () => {
}
};
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 overflow-hidden bg-background text-foreground">
<header className="sticky top-0 z-50 border-b-2 border-foreground bg-background/95 backdrop-blur">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-3 sm:px-6 lg:px-8">
<a href="#top" className="group flex min-w-0 items-center gap-3 font-black uppercase tracking-normal">
<span className="grid h-9 w-9 place-items-center border-2 border-foreground bg-primary text-primary-foreground shadow-[4px_4px_0_hsl(var(--foreground))] transition-transform group-hover:-translate-y-0.5">
<Radio className="h-5 w-5" />
</span>
<span className="hidden text-lg leading-none sm:inline sm:text-xl">LAYER.systems</span>
</a>
<nav className="hidden items-center gap-6 text-sm font-bold uppercase md:flex">
<a href="#relay" className="transition-colors hover:text-primary">Relay</a>
<a href="#features" className="transition-colors hover:text-primary">Signal</a>
<a href="#connect" className="transition-colors hover:text-primary">Connect</a>
<Link to="/explore" className="transition-colors hover:text-primary">Explore</Link>
</nav>
<LoginArea className="max-w-[12rem] sm: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>
<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> */}
{/* 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>
<main id="top">
<section className="border-b-2 border-foreground bg-primary text-primary-foreground">
<div className="relay-ticker flex overflow-hidden py-2 text-sm font-black uppercase">
<div className="relay-ticker-track flex min-w-full shrink-0 items-center gap-8 px-4">
{[...tickerItems, ...tickerItems].map((item, index) => (
<span key={`${item}-${index}`} className="whitespace-nowrap">
{item}
</span>
))}
</div>
<div aria-hidden className="relay-ticker-track flex min-w-full shrink-0 items-center gap-8 px-4">
{[...tickerItems, ...tickerItems].map((item, index) => (
<span key={`clone-${item}-${index}`} className="whitespace-nowrap">
{item}
</span>
))}
</div>
</div>
</section>
{/* 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>
<p className="text-sm text-muted-foreground">
Add this URL to your Nostr client to connect to LAYER.systems
<section className="relative border-b-2 border-foreground bg-[linear-gradient(90deg,hsl(var(--border))_1px,transparent_1px),linear-gradient(180deg,hsl(var(--border))_1px,transparent_1px)] bg-[size:32px_32px]">
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-14 sm:px-6 sm:py-20 lg:grid-cols-[minmax(0,1.1fr)_minmax(340px,0.9fr)] lg:px-8 lg:py-24">
<div className="flex min-w-0 flex-col justify-between gap-10">
<div className="space-y-7">
<div className="inline-flex border-2 border-foreground bg-card px-3 py-1 text-xs font-black uppercase shadow-[3px_3px_0_hsl(var(--primary))]">
Public Nostr relay
</div>
<div className="space-y-5">
<h1 className="max-w-4xl text-5xl font-black leading-[0.92] tracking-normal sm:text-7xl lg:text-8xl">
Relay signal for the open social web.
</h1>
<p className="max-w-2xl text-lg font-medium leading-8 text-muted-foreground sm:text-xl">
LAYER.systems is a public Nostr relay for people and clients that want direct,
portable, protocol-native social infrastructure.
</p>
</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 className="flex flex-col gap-3 sm:flex-row">
<Button asChild size="lg" className="h-12 rounded-none border-2 border-foreground px-5 font-black uppercase shadow-[4px_4px_0_hsl(var(--foreground))] hover:-translate-y-0.5">
<a href="#relay">
Relay URL
<ArrowDown className="h-4 w-4" />
</a>
</Button>
<Button asChild size="lg" variant="outline" className="h-12 rounded-none border-2 border-foreground bg-card px-5 font-black uppercase shadow-[4px_4px_0_hsl(var(--primary))] hover:-translate-y-0.5">
<a href="#connect">Connect client</a>
</Button>
</div>
</div>
<div id="relay" className="self-start lg:mt-12 border-2 border-foreground bg-card p-4 shadow-[8px_8px_0_hsl(var(--primary))]">
<div className="mb-4 flex items-center justify-between gap-4 border-b-2 border-foreground pb-4">
<div>
<p className="text-xs font-black uppercase text-muted-foreground">Relay endpoint</p>
<code className="mt-1 block break-all font-mono text-xl font-bold text-primary sm:text-2xl">
{relayUrl}
</code>
</div>
<Server className="h-8 w-8 shrink-0 text-primary" />
</div>
<Button
size="lg"
onClick={copyToClipboard}
className="mb-5 h-12 w-full rounded-none border-2 border-foreground font-black uppercase"
>
{copied ? <CheckCircle2 className="h-5 w-5" /> : <Copy className="h-5 w-5" />}
{copied ? 'Copied' : 'Copy relay URL'}
</Button>
<dl className="grid grid-cols-2 gap-px overflow-hidden border-2 border-foreground bg-foreground">
{relayStats.map(([label, value]) => (
<div key={label} className="bg-card p-3">
<dt className="text-xs font-black uppercase text-muted-foreground">{label}</dt>
<dd className="mt-1 break-words font-mono text-sm font-bold">{value}</dd>
</div>
))}
</dl>
</div>
</div>
</section>
<section id="features" className="border-b-2 border-foreground bg-card">
<div className="mx-auto max-w-7xl px-4 py-14 sm:px-6 sm:py-20 lg:px-8">
<div className="mb-10 grid gap-5 md:grid-cols-[0.8fr_1.2fr] md:items-end">
<h2 className="text-4xl font-black leading-none sm:text-5xl">Built like relay infrastructure should feel.</h2>
<p className="text-lg font-medium leading-8 text-muted-foreground">
The interface puts the endpoint first: clear, copyable, and surrounded by just enough context
to help humans and Nostr clients get moving.
</p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{features.map((feature) => (
<Card key={feature.title} className="rounded-none border-2 border-foreground shadow-none transition-transform hover:-translate-y-1">
<CardContent className="p-5">
<div className="mb-6 grid h-12 w-12 place-items-center border-2 border-foreground bg-primary text-primary-foreground">
<feature.icon className="h-6 w-6" />
</div>
<h3 className="mb-3 text-xl font-black">{feature.title}</h3>
<p className="leading-7 text-muted-foreground">{feature.description}</p>
</CardContent>
</Card>
))}
</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>
<section id="connect" className="border-b-2 border-foreground bg-secondary">
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-14 sm:px-6 sm:py-20 lg:grid-cols-[0.85fr_1.15fr] lg:px-8">
<div>
<div className="mb-5 inline-flex border-2 border-foreground bg-card px-3 py-1 text-xs font-black uppercase">
Getting started
</div>
<h2 className="text-4xl font-black leading-none sm:text-5xl">Three steps, no platform ceremony.</h2>
</div>
<div className="space-y-4">
{connectionSteps.map((step) => (
<div key={step.label} className="grid gap-4 border-2 border-foreground bg-card p-5 sm:grid-cols-[72px_1fr]">
<div className="font-mono text-3xl font-black text-primary">{step.label}</div>
<div>
<h3 className="text-xl font-black">{step.title}</h3>
<p className="mt-2 leading-7 text-muted-foreground">
{step.description}{' '}
{step.label === '02' && (
<code className="break-all bg-accent px-1.5 py-1 font-mono text-sm text-accent-foreground">
{relayUrl}
</code>
)}
</p>
</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>
))}
</div>
</div>
</div>
</footer>
</section>
<section className="bg-foreground text-background">
<div className="mx-auto flex max-w-7xl flex-col gap-6 px-4 py-10 sm:px-6 md:flex-row md:items-center md:justify-between lg:px-8">
<div className="flex items-center gap-3">
<Activity className="h-6 w-6 text-primary" />
<p className="font-black uppercase">Open relay. Orange signal. Nostr native.</p>
</div>
<div className="flex flex-wrap gap-4 text-sm font-bold">
<Link to="/terms" className="hover:text-primary">Terms</Link>
<Link to="/privacy" className="hover:text-primary">Privacy</Link>
<Link to="/dashboard" className="hover:text-primary">Dashboard</Link>
</div>
</div>
</section>
</main>
</div>
);
};