redesign: complete UI overhaul with Signal design system

Implement warm industrial minimalism with Bitter/Outfit typography,
stone/amber color palette, and consistent Layout/DashboardLayout
components across all pages. Responsive and supports light/dark modes.
This commit is contained in:
2026-03-28 21:43:43 +01:00
parent 4acdb31b8a
commit 98778e77ba
17 changed files with 1252 additions and 1072 deletions

View File

@@ -0,0 +1,44 @@
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/navigation/AppSidebar';
import { Layers } from 'lucide-react';
import { Link } from 'react-router-dom';
import { LoginArea } from '@/components/auth/LoginArea';
interface DashboardLayoutProps {
title: string;
children: React.ReactNode;
}
export function DashboardLayout({ title, children }: DashboardLayoutProps) {
return (
<SidebarProvider>
<div className="flex min-h-screen w-full overflow-x-hidden bg-background">
<AppSidebar />
<main className="flex-1 min-w-0">
{/* Top bar */}
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background/80 backdrop-blur-lg px-4 lg:h-[60px] lg:px-6">
<SidebarTrigger />
<h1 className="flex-1 truncate font-serif text-lg font-semibold md:text-xl">
{title}
</h1>
<div className="hidden sm:flex items-center gap-3">
<Link
to="/"
className="flex items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground"
>
<Layers className="h-3.5 w-3.5" />
Home
</Link>
<LoginArea className="max-w-48" />
</div>
</div>
{/* Page content */}
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
{children}
</div>
</main>
</div>
</SidebarProvider>
);
}

151
src/components/Layout.tsx Normal file
View File

@@ -0,0 +1,151 @@
import { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Menu, X, Layers } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { LoginArea } from '@/components/auth/LoginArea';
const navLinks = [
{ label: 'Home', href: '/' },
{ label: 'Explore', href: '/explore' },
{ label: 'Dashboard', href: '/dashboard' },
];
interface LayoutProps {
children: React.ReactNode;
/** Hide the default footer (e.g. for full-bleed pages) */
hideFooter?: boolean;
}
export function Layout({ children, hideFooter }: LayoutProps) {
const [mobileOpen, setMobileOpen] = useState(false);
const location = useLocation();
return (
<div className="min-h-screen flex flex-col bg-background">
{/* ─── Header ─── */}
<header className="sticky top-0 z-50 border-b bg-background/80 backdrop-blur-lg supports-[backdrop-filter]:bg-background/60">
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4 sm:px-6 lg:px-8">
{/* Logo */}
<Link
to="/"
className="flex items-center gap-2.5 font-serif text-lg font-bold tracking-tight transition-opacity hover:opacity-80"
>
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<Layers className="h-4 w-4" />
</div>
<span>LAYER<span className="text-primary">.systems</span></span>
</Link>
{/* Desktop nav */}
<nav className="hidden items-center gap-1 md:flex">
{navLinks.map((link) => {
const active = location.pathname === link.href ||
(link.href !== '/' && location.pathname.startsWith(link.href));
return (
<Link
key={link.href}
to={link.href}
className={`
rounded-md px-3 py-1.5 text-sm font-medium transition-colors
${active
? 'bg-secondary text-foreground'
: 'text-muted-foreground hover:bg-secondary/60 hover:text-foreground'
}
`}
>
{link.label}
</Link>
);
})}
</nav>
{/* Right side: login + mobile burger */}
<div className="flex items-center gap-3">
<LoginArea className="hidden sm:inline-flex max-w-48" />
<Button
variant="ghost"
size="icon"
className="md:hidden"
onClick={() => setMobileOpen(!mobileOpen)}
aria-label="Toggle menu"
>
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</Button>
</div>
</div>
{/* Mobile nav panel */}
{mobileOpen && (
<div className="border-t bg-background px-4 pb-4 pt-2 md:hidden animate-fade-in">
<nav className="flex flex-col gap-1">
{navLinks.map((link) => {
const active = location.pathname === link.href ||
(link.href !== '/' && location.pathname.startsWith(link.href));
return (
<Link
key={link.href}
to={link.href}
onClick={() => setMobileOpen(false)}
className={`
rounded-md px-3 py-2 text-sm font-medium transition-colors
${active
? 'bg-secondary text-foreground'
: 'text-muted-foreground hover:bg-secondary/60 hover:text-foreground'
}
`}
>
{link.label}
</Link>
);
})}
</nav>
<div className="mt-3 pt-3 border-t">
<LoginArea className="w-full" />
</div>
</div>
)}
</header>
{/* ─── Main content ─── */}
<main className="flex-1">
{children}
</main>
{/* ─── Footer ─── */}
{!hideFooter && (
<footer className="border-t bg-muted/30">
<div className="mx-auto max-w-6xl px-4 py-8 sm:px-6 lg:px-8">
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="flex h-5 w-5 items-center justify-center rounded bg-primary/10">
<Layers className="h-3 w-3 text-primary" />
</div>
<span>&copy; {new Date().getFullYear()} LAYER.systems</span>
</div>
<div className="flex items-center gap-6 text-sm">
<Link
to="/explore"
className="text-muted-foreground transition-colors hover:text-foreground"
>
Explore
</Link>
<Link
to="/terms"
className="text-muted-foreground transition-colors hover:text-foreground"
>
Terms
</Link>
<Link
to="/privacy"
className="text-muted-foreground transition-colors hover:text-foreground"
>
Privacy
</Link>
</div>
</div>
</div>
</footer>
)}
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { Home, LayoutDashboard, FileText, Download } from 'lucide-react';
import { Home, LayoutDashboard, FileText, Download, Layers } from 'lucide-react';
import { Link, useLocation } from 'react-router-dom';
import {
Sidebar,
@@ -7,6 +7,7 @@ import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
@@ -41,9 +42,26 @@ export function AppSidebar() {
return (
<Sidebar>
{/* Brand header */}
<SidebarHeader>
<Link
to="/"
className="flex items-center gap-2.5 px-2 py-1.5 font-serif text-sm font-bold tracking-tight transition-opacity hover:opacity-80"
>
<div className="flex h-7 w-7 items-center justify-center rounded-md bg-primary text-primary-foreground">
<Layers className="h-3.5 w-3.5" />
</div>
<span>
LAYER<span className="text-primary">.systems</span>
</span>
</Link>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupLabel className="text-xs font-medium uppercase tracking-wider text-muted-foreground/70">
Navigation
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{navigationItems.map((item) => {
@@ -52,7 +70,7 @@ export function AppSidebar() {
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild isActive={isActive}>
<Link to={item.url}>
<item.icon />
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
@@ -63,6 +81,7 @@ export function AppSidebar() {
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<div className="p-2">
<LoginArea className="w-full" />

View File

@@ -4,111 +4,112 @@
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 14.9020%;
/* Stone + Amber palette — warm industrial minimalism */
--background: 40 20% 98%;
--foreground: 24 10% 10%;
--card: 0 0% 100%;
--card-foreground: 0 0% 14.9020%;
--card-foreground: 24 10% 10%;
--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%;
--destructive: 0 84.2365% 60.1961%;
--popover-foreground: 24 10% 10%;
--primary: 25 95% 53%;
--primary-foreground: 0 0% 100%;
--secondary: 33 10% 93%;
--secondary-foreground: 24 10% 30%;
--muted: 40 15% 95%;
--muted-foreground: 24 5% 45%;
--accent: 33 10% 93%;
--accent-foreground: 24 10% 20%;
--destructive: 0 84% 60%;
--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%;
--border: 30 10% 88%;
--input: 30 10% 88%;
--ring: 25 95% 53%;
--chart-1: 25 95% 53%;
--chart-2: 32 95% 44%;
--chart-3: 21 90% 48%;
--chart-4: 38 92% 50%;
--chart-5: 15 75% 28%;
--sidebar: 40 15% 96%;
--sidebar-foreground: 24 10% 10%;
--sidebar-primary: 25 95% 53%;
--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;
--font-serif: Source Serif 4, serif;
--font-mono: JetBrains Mono, monospace;
--radius: 0.375rem;
--sidebar-accent: 33 10% 93%;
--sidebar-accent-foreground: 24 10% 20%;
--sidebar-border: 30 10% 88%;
--sidebar-ring: 25 95% 53%;
--font-sans: 'Outfit Variable', 'Outfit', system-ui, sans-serif;
--font-serif: 'Bitter Variable', 'Bitter', Georgia, serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--radius: 0.625rem;
--shadow-x: 0px;
--shadow-y: 4px;
--shadow-blur: 8px;
--shadow-spread: -1px;
--shadow-opacity: 0.1;
--shadow-color: hsl(0 0% 0%);
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
--shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.03);
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.04);
--shadow-sm: 0px 2px 4px -1px hsl(0 0% 0% / 0.06), 0px 1px 2px -1px hsl(0 0% 0% / 0.04);
--shadow: 0px 4px 6px -1px hsl(0 0% 0% / 0.07), 0px 2px 4px -2px hsl(0 0% 0% / 0.05);
--shadow-md: 0px 6px 10px -1px hsl(0 0% 0% / 0.08), 0px 3px 5px -2px hsl(0 0% 0% / 0.05);
--shadow-lg: 0px 10px 15px -3px hsl(0 0% 0% / 0.08), 0px 4px 6px -4px hsl(0 0% 0% / 0.04);
--shadow-xl: 0px 20px 25px -5px hsl(0 0% 0% / 0.08), 0px 8px 10px -6px hsl(0 0% 0% / 0.04);
--shadow-2xl: 0px 25px 50px -12px hsl(0 0% 0% / 0.18);
--tracking-normal: 0em;
--spacing: 0.25rem;
}
.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%;
--destructive: 0 84.2365% 60.1961%;
--background: 20 8% 8%;
--foreground: 40 15% 90%;
--card: 20 8% 12%;
--card-foreground: 40 15% 90%;
--popover: 20 8% 12%;
--popover-foreground: 40 15% 90%;
--primary: 25 95% 53%;
--primary-foreground: 0 0% 100%;
--secondary: 20 6% 18%;
--secondary-foreground: 40 10% 75%;
--muted: 20 6% 15%;
--muted-foreground: 30 6% 55%;
--accent: 20 8% 18%;
--accent-foreground: 38 92% 60%;
--destructive: 0 84% 60%;
--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%;
--border: 20 6% 22%;
--input: 20 6% 22%;
--ring: 25 95% 53%;
--chart-1: 38 92% 60%;
--chart-2: 25 95% 53%;
--chart-3: 15 75% 42%;
--chart-4: 32 95% 44%;
--chart-5: 21 90% 48%;
--sidebar: 20 8% 6%;
--sidebar-foreground: 40 15% 90%;
--sidebar-primary: 25 95% 53%;
--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;
--font-serif: Source Serif 4, serif;
--font-mono: JetBrains Mono, monospace;
--radius: 0.375rem;
--sidebar-accent: 20 8% 18%;
--sidebar-accent-foreground: 38 92% 60%;
--sidebar-border: 20 6% 22%;
--sidebar-ring: 25 95% 53%;
--font-sans: 'Outfit Variable', 'Outfit', system-ui, sans-serif;
--font-serif: 'Bitter Variable', 'Bitter', Georgia, serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--radius: 0.625rem;
--shadow-x: 0px;
--shadow-y: 4px;
--shadow-blur: 8px;
--shadow-spread: -1px;
--shadow-opacity: 0.1;
--shadow-opacity: 0.15;
--shadow-color: hsl(0 0% 0%);
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
--shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.10);
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.15);
--shadow-sm: 0px 2px 4px -1px hsl(0 0% 0% / 0.20), 0px 1px 2px -1px hsl(0 0% 0% / 0.15);
--shadow: 0px 4px 6px -1px hsl(0 0% 0% / 0.25), 0px 2px 4px -2px hsl(0 0% 0% / 0.15);
--shadow-md: 0px 6px 10px -1px hsl(0 0% 0% / 0.30), 0px 3px 5px -2px hsl(0 0% 0% / 0.15);
--shadow-lg: 0px 10px 15px -3px hsl(0 0% 0% / 0.30), 0px 4px 6px -4px hsl(0 0% 0% / 0.15);
--shadow-xl: 0px 20px 25px -5px hsl(0 0% 0% / 0.30), 0px 8px 10px -6px hsl(0 0% 0% / 0.15);
--shadow-2xl: 0px 25px 50px -12px hsl(0 0% 0% / 0.50);
}
}
@@ -118,6 +119,54 @@
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground antialiased;
font-family: var(--font-sans);
}
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-serif);
letter-spacing: -0.01em;
}
}
/* Utility animations */
@layer utilities {
.animate-fade-in {
animation: fade-in 0.5s ease-out forwards;
}
.animate-slide-up {
animation: slide-up 0.6s ease-out forwards;
}
.animate-slide-up-delay-1 {
animation: slide-up 0.6s ease-out 0.1s forwards;
opacity: 0;
}
.animate-slide-up-delay-2 {
animation: slide-up 0.6s ease-out 0.2s forwards;
opacity: 0;
}
.animate-slide-up-delay-3 {
animation: slide-up 0.6s ease-out 0.3s forwards;
opacity: 0;
}
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -3,6 +3,10 @@ import { createRoot } from 'react-dom/client';
// Import polyfills first
import './lib/polyfills.ts';
// Fonts
import '@fontsource-variable/outfit';
import '@fontsource-variable/bitter';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import App from './App.tsx';
import './index.css';

View File

@@ -1,5 +1,4 @@
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/navigation/AppSidebar';
import { DashboardLayout } from '@/components/DashboardLayout';
import { DashboardStats } from '@/components/dashboard/DashboardStats';
import { EventKindsChart } from '@/components/dashboard/EventKindsChart';
import { RecentActivityChart } from '@/components/dashboard/RecentActivityChart';
@@ -17,57 +16,45 @@ export function Dashboard() {
const getDisplayName = (account: Account): string => {
return account.metadata.name ?? genUserName(account.pubkey);
}
};
return (
<SidebarProvider>
<div className="flex min-h-screen w-full overflow-x-hidden">
<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">
<SidebarTrigger />
<h1 className="text-lg font-semibold md:text-xl truncate">Dashboard</h1>
<DashboardLayout title="Dashboard">
{!user ? (
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center">
<div className="max-w-sm mx-auto space-y-4">
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertDescription>
Please log in to view your dashboard and activity statistics.
</AlertDescription>
</Alert>
</div>
</CardContent>
</Card>
) : (
<>
<div className="space-y-1">
<h2 className="font-serif text-2xl font-bold tracking-tight md:text-3xl break-words">
Welcome back{currentUser ? `, ${getDisplayName(currentUser)}` : ''}
</h2>
<p className="text-sm text-muted-foreground md:text-base">
Here's an overview of your Nostr activity and statistics.
</p>
</div>
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
{!user ? (
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center">
<div className="max-w-sm mx-auto space-y-4">
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertDescription>
Please log in to view your dashboard and activity statistics.
</AlertDescription>
</Alert>
</div>
</CardContent>
</Card>
) : (
<>
<div className="space-y-2">
<h2 className="text-2xl md:text-3xl font-bold tracking-tight break-words">
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>
</div>
<DashboardStats pubkey={user.pubkey} />
<DashboardStats pubkey={user.pubkey} />
<RecentActivityChart />
<RecentActivityChart />
<div className="grid gap-6 md:grid-cols-2">
<EventKindsChart />
<RecentActivityList pubkey={user.pubkey} />
</div>
</>
)}
<div className="grid gap-6 md:grid-cols-2">
<EventKindsChart />
<RecentActivityList pubkey={user.pubkey} />
</div>
</main>
</div>
</SidebarProvider>
</>
)}
</DashboardLayout>
);
}

View File

@@ -1,5 +1,4 @@
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/navigation/AppSidebar';
import { DashboardLayout } from '@/components/DashboardLayout';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { Card, CardContent } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
@@ -10,48 +9,36 @@ export function DashboardEvents() {
const { user } = useCurrentUser();
return (
<SidebarProvider>
<div className="flex min-h-screen w-full overflow-x-hidden">
<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">
<SidebarTrigger />
<h1 className="text-lg font-semibold md:text-xl truncate">My Events</h1>
<DashboardLayout title="My Events">
{!user ? (
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center">
<div className="max-w-sm mx-auto space-y-4">
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertDescription>
Please log in to explore your events and manage deletion requests.
</AlertDescription>
</Alert>
</div>
</CardContent>
</Card>
) : (
<div className="space-y-4">
<div className="space-y-1">
<h2 className="font-serif text-2xl font-bold tracking-tight md:text-3xl break-words">
Your Nostr Events
</h2>
<p className="text-sm text-muted-foreground md:text-base max-w-2xl">
Browse all events you have published on Nostr, search through their content,
and publish deletion requests when you want something removed.
</p>
</div>
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
{!user ? (
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center">
<div className="max-w-sm mx-auto space-y-4">
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertDescription>
Please log in to explore your events and manage deletion requests.
</AlertDescription>
</Alert>
</div>
</CardContent>
</Card>
) : (
<div className="space-y-4">
<div className="space-y-2">
<h2 className="text-2xl md:text-3xl font-bold tracking-tight break-words">
Your Nostr events
</h2>
<p className="text-sm md:text-base text-muted-foreground max-w-2xl">
Browse all events you have published on Nostr, search through their content,
and publish deletion requests when you want something removed.
</p>
</div>
<EventExplorer pubkey={user.pubkey} />
</div>
)}
</div>
</main>
</div>
</SidebarProvider>
<EventExplorer pubkey={user.pubkey} />
</div>
)}
</DashboardLayout>
);
}

View File

@@ -1,7 +1,6 @@
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/navigation/AppSidebar';
import { DashboardLayout } from '@/components/DashboardLayout';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useNostr } from '@nostrify/react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -20,13 +19,13 @@ export function DashboardExport() {
queryKey: ['contact-list', user?.pubkey],
queryFn: async (c) => {
if (!user) return null;
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(3000)]);
const events = await nostr.query(
[{ kinds: [3], authors: [user.pubkey], limit: 1 }],
{ signal }
);
return events[0] || null;
},
enabled: !!user,
@@ -88,164 +87,152 @@ export function DashboardExport() {
};
return (
<SidebarProvider>
<div className="flex min-h-screen w-full overflow-x-hidden">
<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">
<SidebarTrigger />
<h1 className="text-lg font-semibold md:text-xl truncate">Export Following List</h1>
</div>
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
{!user ? (
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center">
<div className="max-w-sm mx-auto space-y-4">
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertDescription>
Please log in to export your following list.
</AlertDescription>
</Alert>
<DashboardLayout title="Export Following List">
{!user ? (
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center">
<div className="max-w-sm mx-auto space-y-4">
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertDescription>
Please log in to export your following list.
</AlertDescription>
</Alert>
</div>
</CardContent>
</Card>
) : (
<div className="max-w-2xl mx-auto space-y-6">
<Card>
<CardHeader>
<CardTitle className="font-serif">Backup Your Following List</CardTitle>
<CardDescription>
Export your contact list as a JSON file for backup or migration purposes.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{isLoading ? (
<div className="space-y-4">
<div className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-48" />
</div>
</div>
</CardContent>
</Card>
) : (
<div className="max-w-2xl mx-auto space-y-6">
<Card>
<CardHeader>
<CardTitle>Backup Your Following List</CardTitle>
<CardDescription>
Export your contact list as a JSON file for backup or migration purposes.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{isLoading ? (
<div className="space-y-4">
<div className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-48" />
</div>
</div>
</div>
) : !contactListEvent ? (
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertDescription>
No following list found. You may need to follow some users first.
</AlertDescription>
</Alert>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-48" />
<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>
<div>
<p className="text-sm text-muted-foreground">Following</p>
<p className="text-2xl font-bold">{followingCount}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-48" />
<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>
<div>
<p className="text-sm text-muted-foreground">Last Updated</p>
<p className="text-sm font-medium">
{createdDate
? createdDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
: 'Unknown'}
</p>
<p className="text-xs text-muted-foreground">
{createdDate
? createdDate.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
})
: ''}
</p>
</div>
</div>
</div>
) : !contactListEvent ? (
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertDescription>
No following list found. You may need to follow some users first.
</AlertDescription>
</Alert>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<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>
<div>
<p className="text-sm text-muted-foreground">Following</p>
<p className="text-2xl font-bold">{followingCount}</p>
</div>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
</div>
<Card>
<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>
<div>
<p className="text-sm text-muted-foreground">Last Updated</p>
<p className="text-sm font-medium">
{createdDate
? createdDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
: 'Unknown'}
</p>
<p className="text-xs text-muted-foreground">
{createdDate
? createdDate.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
})
: ''}
</p>
</div>
</div>
</CardContent>
</Card>
</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>
<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>
<li>Relay hints and petnames (if available)</li>
<li>Original event data and signature</li>
<li>Timestamp of when the list was created</li>
</ul>
</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>
<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>
<li>• Relay hints and petnames (if available)</li>
<li>• Original event data and signature</li>
<li>• Timestamp of when the list was created</li>
</ul>
</div>
<Button
onClick={handleExport}
disabled={isExporting || !contactListEvent}
className="w-full"
size="lg"
>
<Download className="mr-2 h-5 w-5" />
{isExporting ? 'Exporting...' : 'Export as JSON'}
</Button>
<Button
onClick={handleExport}
disabled={isExporting || !contactListEvent}
className="w-full"
size="lg"
>
<Download className="mr-2 h-5 w-5" />
{isExporting ? 'Exporting...' : 'Export as JSON'}
</Button>
<p className="text-xs text-muted-foreground text-center">
Your backup file will be saved to your downloads folder
</p>
</div>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">About Your Following List</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground space-y-2">
<p>
Your following list is stored on Nostr as a kind 3 event. This backup includes
the complete event data, allowing you to restore your following list on any
compatible Nostr client.
<p className="text-xs text-muted-foreground text-center">
Your backup file will be saved to your downloads folder
</p>
<p>
The export includes all public keys (npubs) of accounts you follow, along with
optional relay hints and petnames that help clients find and display these
accounts.
</p>
</CardContent>
</Card>
</div>
)}
</div>
</main>
</div>
</SidebarProvider>
</div>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="font-serif text-base">About Your Following List</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground space-y-2">
<p>
Your following list is stored on Nostr as a kind 3 event. This backup includes
the complete event data, allowing you to restore your following list on any
compatible Nostr client.
</p>
<p>
The export includes all public keys (npubs) of accounts you follow, along with
optional relay hints and petnames that help clients find and display these
accounts.
</p>
</CardContent>
</Card>
</div>
)}
</DashboardLayout>
);
}

View File

@@ -9,6 +9,7 @@ import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { NoteContent } from '@/components/NoteContent';
import { genUserName } from '@/lib/genUserName';
import { Layout } from '@/components/Layout';
function TextNoteCard({ event }: { event: NostrEvent }) {
const author = useAuthor(event.pubkey);
@@ -21,31 +22,28 @@ function TextNoteCard({ event }: { event: NostrEvent }) {
const timestamp = new Date(event.created_at * 1000).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
return (
<Card className="overflow-hidden hover:shadow-md transition-shadow">
<Card className="transition-shadow hover:shadow-md">
<CardHeader className="pb-3">
<div className="flex items-start space-x-3">
<Avatar className="h-10 w-10 border-2 border-background">
<div className="flex items-start gap-3">
<Avatar className="h-9 w-9 border border-border">
<AvatarImage src={profileImage} alt={displayName} />
<AvatarFallback>{displayName[0]?.toUpperCase()}</AvatarFallback>
<AvatarFallback className="text-xs">{displayName[0]?.toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<h3 className="font-semibold text-sm truncate">{displayName}</h3>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate text-sm font-semibold">{displayName}</span>
{metadata?.nip05 && (
<Badge variant="secondary" className="text-xs">
</Badge>
<Badge variant="secondary" className="shrink-0 px-1.5 py-0 text-[10px]">NIP-05</Badge>
)}
</div>
<p className="text-xs text-muted-foreground truncate">@{username}</p>
<p className="truncate text-xs text-muted-foreground">@{username}</p>
</div>
<time className="text-xs text-muted-foreground shrink-0">{timestamp}</time>
<time className="shrink-0 text-xs text-muted-foreground">{timestamp}</time>
</div>
</CardHeader>
<CardContent className="pt-0">
@@ -59,7 +57,7 @@ function TextNoteCard({ event }: { event: NostrEvent }) {
function ProfileCard({ event }: { event: NostrEvent }) {
let metadata: NostrMetadata | undefined;
try {
metadata = JSON.parse(event.content) as NostrMetadata;
} catch {
@@ -75,52 +73,44 @@ function ProfileCard({ event }: { event: NostrEvent }) {
const website = metadata?.website;
return (
<Card className="overflow-hidden hover:shadow-md transition-shadow">
{banner && (
<div className="h-24 sm:h-32 bg-gradient-to-br from-primary/20 to-primary/5 relative">
<img
src={banner}
alt=""
className="w-full h-full object-cover"
loading="lazy"
/>
<Card className="overflow-hidden transition-shadow hover:shadow-md">
{banner ? (
<div className="h-24 bg-muted">
<img src={banner} alt="" className="h-full w-full object-cover" loading="lazy" />
</div>
) : (
<div className="h-16 bg-gradient-to-br from-primary/10 to-primary/5" />
)}
<CardHeader className={banner ? '-mt-12 pb-3' : 'pb-3'}>
<div className="flex items-start space-x-4">
<Avatar className="h-20 w-20 border-4 border-background shadow-lg">
<CardHeader className="-mt-10 pb-3">
<div className="flex items-start gap-3">
<Avatar className="h-16 w-16 border-[3px] border-card shadow-sm">
<AvatarImage src={profileImage} alt={displayName} />
<AvatarFallback className="text-2xl">{displayName[0]?.toUpperCase()}</AvatarFallback>
<AvatarFallback className="text-lg">{displayName[0]?.toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0 pt-8">
<div className="flex items-center space-x-2">
<h3 className="font-bold text-lg truncate">{displayName}</h3>
<div className="min-w-0 flex-1 pt-6">
<div className="flex items-center gap-2">
<h3 className="truncate font-serif font-bold">{displayName}</h3>
{nip05 && (
<Badge variant="secondary" className="text-xs">
</Badge>
<Badge variant="secondary" className="shrink-0 px-1.5 py-0 text-[10px]">NIP-05</Badge>
)}
</div>
<p className="text-sm text-muted-foreground truncate">@{username}</p>
{nip05 && (
<p className="text-xs text-muted-foreground/70 truncate mt-1">{nip05}</p>
)}
<p className="truncate text-xs text-muted-foreground">@{username}</p>
</div>
</div>
</CardHeader>
{(about || website) && (
<CardContent className="pt-0 space-y-2">
<CardContent className="space-y-2 pt-0">
{about && (
<p className="text-sm text-muted-foreground line-clamp-3">{about}</p>
<p className="line-clamp-3 text-sm text-muted-foreground">{about}</p>
)}
{website && (
<a
href={website}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary hover:underline inline-flex items-center"
className="inline-block text-sm text-primary hover:underline"
>
🔗 {website.replace(/^https?:\/\//, '')}
{website.replace(/^https?:\/\//, '')}
</a>
)}
</CardContent>
@@ -135,11 +125,11 @@ function LoadingSkeleton() {
{[1, 2, 3].map((i) => (
<Card key={i}>
<CardHeader>
<div className="flex items-center space-x-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-24" />
<div className="flex items-center gap-3">
<Skeleton className="h-9 w-9 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-28" />
<Skeleton className="h-3 w-20" />
</div>
</div>
</CardHeader>
@@ -161,21 +151,19 @@ export function Explore() {
const { data, isLoading, isError } = useExploreEvents();
return (
<div className="min-h-screen bg-gradient-to-b from-background via-background to-muted/20">
<div className="container max-w-4xl mx-auto px-4 py-8 space-y-6">
<Layout>
<div className="mx-auto max-w-3xl px-4 py-10 sm:px-6 lg:px-8">
{/* Header */}
<div className="space-y-2">
<h1 className="text-4xl sm:text-5xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
Explore
</h1>
<p className="text-muted-foreground text-sm sm:text-base">
Discover the latest text notes and profiles from the Nostr
<div className="mb-8">
<h1 className="text-3xl font-bold sm:text-4xl">Explore</h1>
<p className="mt-1 text-muted-foreground">
The latest notes and profiles from the network.
</p>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-2 max-w-md mx-auto">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="mb-6 grid w-full max-w-xs grid-cols-2">
<TabsTrigger value="notes">
Notes ({data?.textNotes?.length || 0})
</TabsTrigger>
@@ -184,15 +172,15 @@ export function Explore() {
</TabsTrigger>
</TabsList>
{/* Text Notes Tab */}
<TabsContent value="notes" className="mt-6 space-y-4">
{/* Notes Tab */}
<TabsContent value="notes" className="space-y-4">
{isLoading && <LoadingSkeleton />}
{isError && (
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center">
<CardContent className="py-12 text-center">
<p className="text-muted-foreground">
Unable to load notes. Please check your relay connections.
Unable to load notes. Check your relay connections.
</p>
</CardContent>
</Card>
@@ -200,9 +188,9 @@ export function Explore() {
{!isLoading && !isError && data?.textNotes.length === 0 && (
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center">
<CardContent className="py-12 text-center">
<p className="text-muted-foreground">
No text notes found. Try refreshing or check your relay connections.
No notes found. Try refreshing or check your relay connections.
</p>
</CardContent>
</Card>
@@ -214,18 +202,18 @@ export function Explore() {
</TabsContent>
{/* Profiles Tab */}
<TabsContent value="profiles" className="mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<TabsContent value="profiles">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{isLoading && (
<>
{[1, 2, 3, 4].map((i) => (
<Card key={i}>
<CardHeader>
<div className="flex items-center space-x-3">
<Skeleton className="h-20 w-20 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-24" />
<div className="flex items-center gap-3">
<Skeleton className="h-16 w-16 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-28" />
<Skeleton className="h-3 w-20" />
</div>
</div>
</CardHeader>
@@ -237,9 +225,9 @@ export function Explore() {
{isError && (
<div className="col-span-full">
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center">
<CardContent className="py-12 text-center">
<p className="text-muted-foreground">
Unable to load profiles. Please check your relay connections.
Unable to load profiles. Check your relay connections.
</p>
</CardContent>
</Card>
@@ -249,9 +237,9 @@ export function Explore() {
{!isLoading && !isError && data?.profiles.length === 0 && (
<div className="col-span-full">
<Card className="border-dashed">
<CardContent className="py-12 px-8 text-center">
<CardContent className="py-12 text-center">
<p className="text-muted-foreground">
No profiles found. Try refreshing or check your relay connections.
No profiles found.
</p>
</CardContent>
</Card>
@@ -265,6 +253,6 @@ export function Explore() {
</TabsContent>
</Tabs>
</div>
</div>
</Layout>
);
}

View File

@@ -1,10 +1,11 @@
import { useSeoMeta } from '@unhead/react';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { CheckCircle2, Copy, Server, Gift, Users, Globe } from 'lucide-react';
import { CheckCircle2, Copy, Server, Gift, Users, Globe, ArrowRight, Compass } from 'lucide-react';
import { useToast } from '@/hooks/useToast';
import { LoginArea } from '@/components/auth/LoginArea';
import { Layout } from '@/components/Layout';
const Index = () => {
const [copied, setCopied] = useState(false);
@@ -37,205 +38,180 @@ const Index = () => {
const features = [
{
icon: Gift,
title: 'Free',
description: 'No cost to use - accessible for everyone',
title: 'Free to use',
description: 'No signup, no fees. Open infrastructure for everyone.',
},
{
icon: Users,
title: 'Community driven',
description: 'Built and maintained by the Nostr community',
description: 'Built and maintained by the Nostr community.',
},
{
icon: Globe,
title: 'Global Network',
description: 'Part of the decentralized Nostr ecosystem',
title: 'Global network',
description: 'Part of the decentralized Nostr ecosystem.',
},
{
icon: Server,
title: 'Open Access',
description: 'Free to use for all Nostr clients',
title: 'Open access',
description: 'Compatible with every Nostr client out there.',
},
];
const steps = [
{
num: '01',
title: 'Choose a client',
description: 'Pick a Nostr client like Damus, Amethyst, Snort, or any compatible application.',
},
{
num: '02',
title: 'Add the relay',
description: 'In your client settings, paste our relay URL into your relay list.',
},
{
num: '03',
title: 'Start connecting',
description: 'Post, follow, and engage with the global Nostr network.',
},
];
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>
</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" />
<Layout>
{/* ─── Hero ─── */}
<section className="relative overflow-hidden">
{/* Abstract background shapes */}
<div className="pointer-events-none absolute inset-0">
<div className="absolute -top-24 -right-24 h-[480px] w-[480px] rounded-full bg-primary/[0.04] blur-3xl" />
<div className="absolute -bottom-32 -left-32 h-[400px] w-[400px] rounded-full bg-primary/[0.03] blur-3xl" />
</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
<div className="relative mx-auto max-w-6xl px-4 pb-20 pt-16 sm:px-6 sm:pb-28 sm:pt-24 lg:px-8">
{/* Badge */}
<div className="animate-slide-up mb-6 flex justify-center">
<span className="inline-flex items-center gap-2 rounded-full border bg-card px-4 py-1.5 text-xs font-medium text-muted-foreground shadow-sm">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500" />
</span>
Relay online
</span>
</div>
{/* Headline */}
<div className="animate-slide-up-delay-1 mx-auto max-w-3xl text-center">
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl lg:text-7xl">
Your gateway to the{' '}
<span className="text-primary">open social web</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 className="mt-6 text-lg text-muted-foreground sm:text-xl">
A fast, reliable Nostr relay connecting you to the future of decentralized social media.
</p>
</div>
{/* 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>
{/* Relay URL card */}
<div className="animate-slide-up-delay-2 mx-auto mt-12 max-w-xl">
<Card className="border-primary/20 shadow-lg">
<CardContent className="p-6">
<p className="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
Relay URL
</p>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<code className="flex-1 break-all rounded-md bg-muted px-3 py-2 font-mono text-sm text-primary">
{relayUrl}
</code>
<Button
onClick={copyToClipboard}
className="shrink-0 gap-2"
>
{copied ? (
<><CheckCircle2 className="h-4 w-4" /> Copied</>
) : (
<><Copy className="h-4 w-4" /> Copy URL</>
)}
</Button>
</div>
</CardContent>
</Card>
</div>
{/* CTAs */}
<div className="animate-slide-up-delay-3 mt-8 flex flex-wrap items-center justify-center gap-3">
<Button variant="outline" asChild>
<Link to="/explore" className="gap-2">
<Compass className="h-4 w-4" />
Explore the network
</Link>
</Button>
<Button variant="ghost" asChild>
<Link to="/dashboard" className="gap-2">
Dashboard <ArrowRight className="h-4 w-4" />
</Link>
</Button>
</div>
</div>
</section>
{/* ─── Features ─── */}
<section className="border-t bg-muted/20">
<div className="mx-auto max-w-6xl px-4 py-20 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl font-bold sm:text-4xl">Why LAYER.systems?</h2>
<p className="mt-3 text-muted-foreground">
Infrastructure you can rely on, built for the decentralized future.
</p>
</div>
<div className="mt-14 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{features.map((feature, i) => (
<Card
key={i}
className="group border-transparent bg-card/60 transition-all duration-300 hover:border-border hover:shadow-md"
>
<CardContent className="p-6">
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 transition-colors group-hover:bg-primary/15">
<feature.icon className="h-5 w-5 text-primary" />
</div>
<p className="text-sm text-muted-foreground">
Add this URL to your Nostr client to connect to LAYER.systems
<h3 className="font-serif text-base font-semibold">{feature.title}</h3>
<p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">
{feature.description}
</p>
</div>
</CardContent>
</Card>
</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>
</CardContent>
</Card>
))}
</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.
{/* ─── Getting Started ─── */}
<section className="border-t">
<div className="mx-auto max-w-4xl px-4 py-20 sm:px-6 lg:px-8">
<div className="text-center">
<h2 className="text-3xl font-bold sm:text-4xl">Get started in minutes</h2>
<p className="mt-3 text-muted-foreground">
Three steps to join the Nostr network through our relay.
</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="mt-14 space-y-6">
{steps.map((step) => (
<div
key={step.num}
className="flex items-start gap-5 rounded-xl border bg-card p-6 transition-shadow hover:shadow-sm"
>
<span className="shrink-0 font-mono text-2xl font-bold text-primary/40">
{step.num}
</span>
<div>
<h3 className="font-serif text-lg font-semibold">{step.title}</h3>
<p className="mt-1 text-sm text-muted-foreground">{step.description}</p>
</div>
</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>
</div>
</section>
</Layout>
);
};

View File

@@ -1,5 +1,8 @@
import { nip19 } from 'nostr-tools';
import { useParams } from 'react-router-dom';
import { Layout } from '@/components/Layout';
import { Card, CardContent } from '@/components/ui/card';
import { User, FileText, Radio, BookOpen } from 'lucide-react';
import NotFound from './NotFound';
export function NIP19Page() {
@@ -18,25 +21,86 @@ export function NIP19Page() {
const { type } = decoded;
switch (type) {
case 'npub':
case 'nprofile':
// AI agent should implement profile view here
return <div>Profile placeholder</div>;
const renderContent = () => {
switch (type) {
case 'npub':
case 'nprofile':
return (
<PlaceholderCard
icon={<User className="h-6 w-6" />}
title="Profile"
description="This profile view is not yet implemented."
identifier={identifier}
/>
);
case 'note':
// AI agent should implement note view here
return <div>Note placeholder</div>;
case 'note':
return (
<PlaceholderCard
icon={<FileText className="h-6 w-6" />}
title="Note"
description="This note view is not yet implemented."
identifier={identifier}
/>
);
case 'nevent':
// AI agent should implement event view here
return <div>Event placeholder</div>;
case 'nevent':
return (
<PlaceholderCard
icon={<Radio className="h-6 w-6" />}
title="Event"
description="This event view is not yet implemented."
identifier={identifier}
/>
);
case 'naddr':
// AI agent should implement addressable event view here
return <div>Addressable event placeholder</div>;
case 'naddr':
return (
<PlaceholderCard
icon={<BookOpen className="h-6 w-6" />}
title="Addressable Event"
description="This addressable event view is not yet implemented."
identifier={identifier}
/>
);
default:
return <NotFound />;
}
}
default:
return <NotFound />;
}
};
return (
<Layout>
<section className="mx-auto max-w-3xl px-4 py-16 sm:px-6 lg:px-8">
{renderContent()}
</section>
</Layout>
);
}
function PlaceholderCard({
icon,
title,
description,
identifier,
}: {
icon: React.ReactNode;
title: string;
description: string;
identifier: string;
}) {
return (
<Card>
<CardContent className="py-12 text-center">
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-primary/10 text-primary">
{icon}
</div>
<h1 className="font-serif text-2xl font-bold tracking-tight">{title}</h1>
<p className="mt-2 text-muted-foreground">{description}</p>
<div className="mt-6 rounded-lg bg-muted/50 px-4 py-3">
<p className="text-xs text-muted-foreground break-all font-mono">{identifier}</p>
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,13 +1,16 @@
import { useSeoMeta } from "@unhead/react";
import { useLocation } from "react-router-dom";
import { useLocation, Link } from "react-router-dom";
import { useEffect } from "react";
import { Layout } from "@/components/Layout";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
const NotFound = () => {
const location = useLocation();
useSeoMeta({
title: "404 - Page Not Found",
description: "The page you are looking for could not be found. Return to the home page to continue browsing.",
description: "The page you are looking for could not be found.",
});
useEffect(() => {
@@ -18,15 +21,21 @@ const NotFound = () => {
}, [location.pathname]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-gray-100">404</h1>
<p className="text-xl text-gray-600 dark:text-gray-400 mb-4">Oops! Page not found</p>
<a href="/" className="text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline">
Return to Home
</a>
<Layout>
<div className="flex flex-1 flex-col items-center justify-center px-4 py-32 text-center">
<p className="font-mono text-7xl font-bold text-primary/20 sm:text-9xl">404</p>
<h1 className="mt-4 text-2xl font-bold sm:text-3xl">Page not found</h1>
<p className="mt-2 max-w-md text-muted-foreground">
The page you're looking for doesn't exist or has been moved.
</p>
<Button asChild variant="outline" className="mt-8 gap-2">
<Link to="/">
<ArrowLeft className="h-4 w-4" />
Back to home
</Link>
</Button>
</div>
</div>
</Layout>
);
};

View File

@@ -1,7 +1,6 @@
import { useSeoMeta } from '@unhead/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Shield } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Layout } from '@/components/Layout';
export function Privacy() {
useSeoMeta({
@@ -9,269 +8,202 @@ export function Privacy() {
description: 'Privacy Policy for LAYER.systems Nostr relay',
});
const sections = [
{
title: 'Introduction',
content: (
<p>
LAYER.systems is committed to protecting your privacy. This Privacy Policy explains how we collect, use, and safeguard information when you use our Nostr relay service.
</p>
),
},
{
title: '1. Information We Collect',
content: (
<>
<h4 className="mb-2 font-semibold text-foreground">Nostr Events</h4>
<p>As a Nostr relay, we receive and store events published to our service. These events are public by design and include:</p>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li>Event content and metadata</li>
<li>Public keys (npub) of event authors</li>
<li>Timestamps and event signatures</li>
<li>Tags and references to other events or users</li>
</ul>
<h4 className="mb-2 mt-5 font-semibold text-foreground">Technical Information</h4>
<p>When you connect to our relay, we may collect:</p>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li>IP addresses for security and rate limiting purposes</li>
<li>Connection timestamps</li>
<li>WebSocket connection metadata</li>
<li>Request patterns and usage statistics</li>
</ul>
<h4 className="mb-2 mt-5 font-semibold text-foreground">Website Analytics</h4>
<p>Our website may use analytics tools to understand how users interact with our service. This may include:</p>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li>Page views and navigation patterns</li>
<li>Browser type and device information</li>
<li>Referrer information</li>
</ul>
</>
),
},
{
title: '2. How We Use Information',
content: (
<>
<p>We use collected information to:</p>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li>Provide and maintain the relay service</li>
<li>Improve service performance and reliability</li>
<li>Prevent abuse, spam, and malicious activity</li>
<li>Comply with legal obligations</li>
<li>Analyze usage patterns to improve the service</li>
</ul>
</>
),
},
{
title: '3. Data Storage and Security',
content: (
<>
<p>
<strong className="text-foreground">Public Data:</strong> Events published to our relay are public and may be replicated by other relays in the Nostr network. Once published, events cannot be guaranteed to be deleted from all relays.
</p>
<p className="mt-3">
<strong className="text-foreground">Security Measures:</strong> We implement reasonable security measures to protect our infrastructure, including:
</p>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li>Encrypted connections (WSS/TLS)</li>
<li>Rate limiting and abuse prevention</li>
<li>Regular security audits</li>
<li>Secure server configuration</li>
</ul>
<p className="mt-3">
However, no method of transmission over the internet is 100% secure, and we cannot guarantee absolute security.
</p>
</>
),
},
{
title: '4. Data Retention',
content: (
<>
<p>We retain Nostr events according to our relay policies, which may vary based on:</p>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li>Event kind and type</li>
<li>Storage capacity and resource constraints</li>
<li>Content moderation decisions</li>
<li>Legal requirements</li>
</ul>
<p className="mt-3">
Technical logs (IP addresses, connection data) are typically retained for 30-90 days for security and operational purposes.
</p>
</>
),
},
{
title: '5. Third-Party Services',
content: (
<>
<p>Our service may interact with third-party services, including:</p>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li>Other Nostr relays in the network</li>
<li>Content delivery networks (CDNs)</li>
<li>Analytics providers</li>
<li>Infrastructure providers</li>
</ul>
<p className="mt-3">These third parties have their own privacy policies, and we encourage you to review them.</p>
</>
),
},
{
title: '6. Your Rights and Choices',
content: (
<>
<p>
<strong className="text-foreground">Content Control:</strong> You control the events you publish. Use deletion events (kind 5) to request deletion of your content, though we cannot guarantee deletion from all relays in the network.
</p>
<p className="mt-3">
<strong className="text-foreground">Pseudonymity:</strong> Nostr uses public key cryptography. Your public key (npub) serves as your identity, and you can generate new keys at any time to maintain pseudonymity.
</p>
<p className="mt-3">
<strong className="text-foreground">Data Access:</strong> All events published to our relay are publicly queryable through standard Nostr protocols.
</p>
</>
),
},
{
title: '7. Cookies and Tracking',
content: (
<>
<p>Our website uses local storage and may use cookies to:</p>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li>Store user preferences (theme, settings)</li>
<li>Maintain login sessions</li>
<li>Remember relay configurations</li>
</ul>
<p className="mt-3">You can control cookies through your browser settings. Disabling cookies may affect some functionality.</p>
</>
),
},
{
title: '8. Children\'s Privacy',
content: (
<p>
Our service is not directed to children under 13 years of age. We do not knowingly collect personal information from children under 13. If you believe a child has provided us with personal information, please contact us.
</p>
),
},
{
title: '9. International Users',
content: (
<p>
LAYER.systems may be accessed from anywhere in the world. By using our service, you consent to the transfer of your information to our servers, which may be located in different jurisdictions.
</p>
),
},
{
title: '10. Changes to This Privacy Policy',
content: (
<p>
We may update this Privacy Policy from time to time. Changes will be posted on this page with an updated revision date. Your continued use of the service after changes constitutes acceptance of the updated policy.
</p>
),
},
{
title: '11. Contact Us',
content: (
<p>
If you have questions or concerns about this Privacy Policy or our privacy practices, please contact us through the Nostr protocol or via our website.
</p>
),
},
];
return (
<div className="min-h-screen bg-gradient-to-br from-background via-background to-muted/20">
{/* Header */}
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto px-4 py-4">
<Link to="/" className="inline-flex items-center gap-2 text-xl font-bold hover:opacity-80 transition-opacity">
<Shield className="h-6 w-6" />
<span>LAYER.systems</span>
</Link>
</div>
</header>
{/* Main Content */}
<main className="container mx-auto px-4 py-12 max-w-4xl">
<div className="space-y-8">
{/* Hero Section */}
<div className="text-center space-y-4 pb-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-4">
<Shield className="h-8 w-8 text-primary" />
</div>
<h1 className="text-4xl font-bold tracking-tight">Privacy Policy</h1>
<p className="text-muted-foreground text-lg">
Last updated: December 27, 2025
</p>
</div>
{/* Introduction */}
<Card>
<CardHeader>
<CardTitle>Introduction</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
LAYER.systems is committed to protecting your privacy. This Privacy Policy explains how we collect, use, and safeguard information when you use our Nostr relay service.
</p>
</CardContent>
</Card>
{/* Information We Collect */}
<Card>
<CardHeader>
<CardTitle>1. Information We Collect</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<h3 className="text-lg font-semibold mt-4">Nostr Events</h3>
<p>
As a Nostr relay, we receive and store events published to our service. These events are public by design and include:
</p>
<ul>
<li>Event content and metadata</li>
<li>Public keys (npub) of event authors</li>
<li>Timestamps and event signatures</li>
<li>Tags and references to other events or users</li>
</ul>
<h3 className="text-lg font-semibold mt-4">Technical Information</h3>
<p>
When you connect to our relay, we may collect:
</p>
<ul>
<li>IP addresses for security and rate limiting purposes</li>
<li>Connection timestamps</li>
<li>WebSocket connection metadata</li>
<li>Request patterns and usage statistics</li>
</ul>
<h3 className="text-lg font-semibold mt-4">Website Analytics</h3>
<p>
Our website may use analytics tools to understand how users interact with our service. This may include:
</p>
<ul>
<li>Page views and navigation patterns</li>
<li>Browser type and device information</li>
<li>Referrer information</li>
</ul>
</CardContent>
</Card>
{/* How We Use Information */}
<Card>
<CardHeader>
<CardTitle>2. How We Use Information</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
We use collected information to:
</p>
<ul>
<li>Provide and maintain the relay service</li>
<li>Improve service performance and reliability</li>
<li>Prevent abuse, spam, and malicious activity</li>
<li>Comply with legal obligations</li>
<li>Analyze usage patterns to improve the service</li>
</ul>
</CardContent>
</Card>
{/* Data Storage and Security */}
<Card>
<CardHeader>
<CardTitle>3. Data Storage and Security</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
<strong>Public Data:</strong> Events published to our relay are public and may be replicated by other relays in the Nostr network. Once published, events cannot be guaranteed to be deleted from all relays.
</p>
<p>
<strong>Security Measures:</strong> We implement reasonable security measures to protect our infrastructure, including:
</p>
<ul>
<li>Encrypted connections (WSS/TLS)</li>
<li>Rate limiting and abuse prevention</li>
<li>Regular security audits</li>
<li>Secure server configuration</li>
</ul>
<p>
However, no method of transmission over the internet is 100% secure, and we cannot guarantee absolute security.
</p>
</CardContent>
</Card>
{/* Data Retention */}
<Card>
<CardHeader>
<CardTitle>4. Data Retention</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
We retain Nostr events according to our relay policies, which may vary based on:
</p>
<ul>
<li>Event kind and type</li>
<li>Storage capacity and resource constraints</li>
<li>Content moderation decisions</li>
<li>Legal requirements</li>
</ul>
<p>
Technical logs (IP addresses, connection data) are typically retained for 30-90 days for security and operational purposes.
</p>
</CardContent>
</Card>
{/* Third-Party Services */}
<Card>
<CardHeader>
<CardTitle>5. Third-Party Services</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
Our service may interact with third-party services, including:
</p>
<ul>
<li>Other Nostr relays in the network</li>
<li>Content delivery networks (CDNs)</li>
<li>Analytics providers</li>
<li>Infrastructure providers</li>
</ul>
<p>
These third parties have their own privacy policies, and we encourage you to review them.
</p>
</CardContent>
</Card>
{/* Your Rights */}
<Card>
<CardHeader>
<CardTitle>6. Your Rights and Choices</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
<strong>Content Control:</strong> You control the events you publish. Use deletion events (kind 5) to request deletion of your content, though we cannot guarantee deletion from all relays in the network.
</p>
<p>
<strong>Pseudonymity:</strong> Nostr uses public key cryptography. Your public key (npub) serves as your identity, and you can generate new keys at any time to maintain pseudonymity.
</p>
<p>
<strong>Data Access:</strong> All events published to our relay are publicly queryable through standard Nostr protocols.
</p>
</CardContent>
</Card>
{/* Cookies and Tracking */}
<Card>
<CardHeader>
<CardTitle>7. Cookies and Tracking</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
Our website uses local storage and may use cookies to:
</p>
<ul>
<li>Store user preferences (theme, settings)</li>
<li>Maintain login sessions</li>
<li>Remember relay configurations</li>
</ul>
<p>
You can control cookies through your browser settings. Disabling cookies may affect some functionality.
</p>
</CardContent>
</Card>
{/* Children's Privacy */}
<Card>
<CardHeader>
<CardTitle>8. Children's Privacy</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
Our service is not directed to children under 13 years of age. We do not knowingly collect personal information from children under 13. If you believe a child has provided us with personal information, please contact us.
</p>
</CardContent>
</Card>
{/* International Users */}
<Card>
<CardHeader>
<CardTitle>9. International Users</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
LAYER.systems may be accessed from anywhere in the world. By using our service, you consent to the transfer of your information to our servers, which may be located in different jurisdictions.
</p>
</CardContent>
</Card>
{/* Changes to Privacy Policy */}
<Card>
<CardHeader>
<CardTitle>10. Changes to This Privacy Policy</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
We may update this Privacy Policy from time to time. Changes will be posted on this page with an updated revision date. Your continued use of the service after changes constitutes acceptance of the updated policy.
</p>
</CardContent>
</Card>
{/* Contact */}
<Card>
<CardHeader>
<CardTitle>11. Contact Us</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
If you have questions or concerns about this Privacy Policy or our privacy practices, please contact us through the Nostr protocol or via our website.
</p>
</CardContent>
</Card>
<Layout>
<div className="mx-auto max-w-3xl px-4 py-12 sm:px-6 lg:px-8">
<div className="mb-10 text-center">
<h1 className="text-3xl font-bold sm:text-4xl">Privacy Policy</h1>
<p className="mt-2 text-sm text-muted-foreground">Last updated: December 27, 2025</p>
</div>
{/* Footer Navigation */}
<div className="mt-12 pt-8 border-t text-center space-y-4">
<div className="flex justify-center gap-6 text-sm">
<Link to="/" className="text-muted-foreground hover:text-foreground transition-colors">
Home
</Link>
<Link to="/terms" className="text-muted-foreground hover:text-foreground transition-colors">
Terms of Service
</Link>
</div>
<div className="space-y-5">
{sections.map((section, i) => (
<Card key={i}>
<CardHeader className="pb-3">
<CardTitle className="text-base">{section.title}</CardTitle>
</CardHeader>
<CardContent className="text-sm leading-relaxed text-muted-foreground">
{section.content}
</CardContent>
</Card>
))}
</div>
</main>
</div>
</div>
</Layout>
);
}

View File

@@ -1,7 +1,6 @@
import { useSeoMeta } from '@unhead/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollText } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Layout } from '@/components/Layout';
export function Terms() {
useSeoMeta({
@@ -9,205 +8,150 @@ export function Terms() {
description: 'Terms of Service for LAYER.systems Nostr relay',
});
const sections = [
{
title: 'Introduction',
content: (
<p>
Welcome to LAYER.systems. By accessing and using our Nostr relay service, you agree to be bound by these Terms of Service. Please read them carefully.
</p>
),
},
{
title: '1. Service Description',
content: (
<>
<p>
LAYER.systems provides a public Nostr relay service that allows users to publish and query events on the Nostr protocol. The service is provided "as is" and we make no guarantees about uptime, data persistence, or availability.
</p>
<ul className="mt-3 list-disc space-y-1 pl-5">
<li>The relay may be temporarily unavailable due to maintenance or technical issues</li>
<li>Events may be deleted or modified at our discretion</li>
<li>We reserve the right to refuse service to any user</li>
</ul>
</>
),
},
{
title: '2. User Conduct',
content: (
<>
<p>By using LAYER.systems, you agree not to:</p>
<ul className="mt-3 list-disc space-y-1 pl-5">
<li>Publish illegal content or content that violates applicable laws</li>
<li>Engage in spam, phishing, or other abusive behaviors</li>
<li>Attempt to disrupt or compromise the security of the relay</li>
<li>Use the service to harass, threaten, or harm others</li>
<li>Impersonate others or misrepresent your identity</li>
<li>Violate intellectual property rights</li>
</ul>
<p className="mt-3">We reserve the right to remove content and ban users who violate these terms.</p>
</>
),
},
{
title: '3. Content Policy',
content: (
<>
<p>
You retain all rights to content you publish through our relay. However, by publishing content, you grant us a non-exclusive, worldwide license to store, cache, and distribute your content as necessary to operate the service.
</p>
<p className="mt-3">We may remove content that:</p>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li>Violates applicable laws or regulations</li>
<li>Infringes on intellectual property rights</li>
<li>Contains malware or malicious code</li>
<li>Violates our content policies</li>
</ul>
</>
),
},
{
title: '4. Limitation of Liability',
content: (
<>
<p>
LAYER.systems is provided "as is" without warranties of any kind. To the fullest extent permitted by law, we disclaim all warranties, express or implied.
</p>
<p className="mt-3">We are not liable for:</p>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li>Any data loss or corruption</li>
<li>Service interruptions or downtime</li>
<li>Content published by users</li>
<li>Indirect, incidental, or consequential damages</li>
<li>Loss of profits or revenue</li>
</ul>
</>
),
},
{
title: '5. Privacy',
content: (
<p>
Your use of LAYER.systems is also governed by our{' '}
<a href="/privacy" className="text-primary hover:underline">
Privacy Policy
</a>
. Please review it to understand how we collect and use information.
</p>
),
},
{
title: '6. Changes to Terms',
content: (
<p>
We reserve the right to modify these Terms of Service at any time. Changes will be effective immediately upon posting. Your continued use of the service after changes constitutes acceptance of the modified terms.
</p>
),
},
{
title: '7. Termination',
content: (
<p>
We may terminate or suspend your access to the service at any time, without prior notice, for any reason, including violation of these Terms of Service.
</p>
),
},
{
title: '8. Governing Law',
content: (
<p>
These Terms of Service are governed by and construed in accordance with applicable laws. Any disputes shall be resolved in the appropriate courts.
</p>
),
},
{
title: '9. Contact',
content: (
<p>
If you have questions about these Terms of Service, please contact us through the Nostr protocol or via our website.
</p>
),
},
];
return (
<div className="min-h-screen bg-gradient-to-br from-background via-background to-muted/20">
{/* Header */}
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto px-4 py-4">
<Link to="/" className="inline-flex items-center gap-2 text-xl font-bold hover:opacity-80 transition-opacity">
<ScrollText className="h-6 w-6" />
<span>LAYER.systems</span>
</Link>
</div>
</header>
{/* Main Content */}
<main className="container mx-auto px-4 py-12 max-w-4xl">
<div className="space-y-8">
{/* Hero Section */}
<div className="text-center space-y-4 pb-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-4">
<ScrollText className="h-8 w-8 text-primary" />
</div>
<h1 className="text-4xl font-bold tracking-tight">Terms of Service</h1>
<p className="text-muted-foreground text-lg">
Last updated: December 27, 2025
</p>
</div>
{/* Introduction */}
<Card>
<CardHeader>
<CardTitle>Introduction</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
Welcome to LAYER.systems. By accessing and using our Nostr relay service, you agree to be bound by these Terms of Service. Please read them carefully.
</p>
</CardContent>
</Card>
{/* Service Description */}
<Card>
<CardHeader>
<CardTitle>1. Service Description</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
LAYER.systems provides a public Nostr relay service that allows users to publish and query events on the Nostr protocol. The service is provided "as is" and we make no guarantees about uptime, data persistence, or availability.
</p>
<ul>
<li>The relay may be temporarily unavailable due to maintenance or technical issues</li>
<li>Events may be deleted or modified at our discretion</li>
<li>We reserve the right to refuse service to any user</li>
</ul>
</CardContent>
</Card>
{/* User Conduct */}
<Card>
<CardHeader>
<CardTitle>2. User Conduct</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
By using LAYER.systems, you agree not to:
</p>
<ul>
<li>Publish illegal content or content that violates applicable laws</li>
<li>Engage in spam, phishing, or other abusive behaviors</li>
<li>Attempt to disrupt or compromise the security of the relay</li>
<li>Use the service to harass, threaten, or harm others</li>
<li>Impersonate others or misrepresent your identity</li>
<li>Violate intellectual property rights</li>
</ul>
<p>
We reserve the right to remove content and ban users who violate these terms.
</p>
</CardContent>
</Card>
{/* Content Policy */}
<Card>
<CardHeader>
<CardTitle>3. Content Policy</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
You retain all rights to content you publish through our relay. However, by publishing content, you grant us a non-exclusive, worldwide license to store, cache, and distribute your content as necessary to operate the service.
</p>
<p>
We may remove content that:
</p>
<ul>
<li>Violates applicable laws or regulations</li>
<li>Infringes on intellectual property rights</li>
<li>Contains malware or malicious code</li>
<li>Violates our content policies</li>
</ul>
</CardContent>
</Card>
{/* Limitation of Liability */}
<Card>
<CardHeader>
<CardTitle>4. Limitation of Liability</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
LAYER.systems is provided "as is" without warranties of any kind. To the fullest extent permitted by law, we disclaim all warranties, express or implied.
</p>
<p>
We are not liable for:
</p>
<ul>
<li>Any data loss or corruption</li>
<li>Service interruptions or downtime</li>
<li>Content published by users</li>
<li>Indirect, incidental, or consequential damages</li>
<li>Loss of profits or revenue</li>
</ul>
</CardContent>
</Card>
{/* Privacy */}
<Card>
<CardHeader>
<CardTitle>5. Privacy</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
Your use of LAYER.systems is also governed by our{' '}
<Link to="/privacy" className="text-primary hover:underline">
Privacy Policy
</Link>
. Please review it to understand how we collect and use information.
</p>
</CardContent>
</Card>
{/* Changes to Terms */}
<Card>
<CardHeader>
<CardTitle>6. Changes to Terms</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
We reserve the right to modify these Terms of Service at any time. Changes will be effective immediately upon posting. Your continued use of the service after changes constitutes acceptance of the modified terms.
</p>
</CardContent>
</Card>
{/* Termination */}
<Card>
<CardHeader>
<CardTitle>7. Termination</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
We may terminate or suspend your access to the service at any time, without prior notice, for any reason, including violation of these Terms of Service.
</p>
</CardContent>
</Card>
{/* Governing Law */}
<Card>
<CardHeader>
<CardTitle>8. Governing Law</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
These Terms of Service are governed by and construed in accordance with applicable laws. Any disputes shall be resolved in the appropriate courts.
</p>
</CardContent>
</Card>
{/* Contact */}
<Card>
<CardHeader>
<CardTitle>9. Contact</CardTitle>
</CardHeader>
<CardContent className="prose prose-neutral dark:prose-invert max-w-none">
<p>
If you have questions about these Terms of Service, please contact us through the Nostr protocol or via our website.
</p>
</CardContent>
</Card>
<Layout>
<div className="mx-auto max-w-3xl px-4 py-12 sm:px-6 lg:px-8">
<div className="mb-10 text-center">
<h1 className="text-3xl font-bold sm:text-4xl">Terms of Service</h1>
<p className="mt-2 text-sm text-muted-foreground">Last updated: December 27, 2025</p>
</div>
{/* Footer Navigation */}
<div className="mt-12 pt-8 border-t text-center space-y-4">
<div className="flex justify-center gap-6 text-sm">
<Link to="/" className="text-muted-foreground hover:text-foreground transition-colors">
Home
</Link>
<Link to="/privacy" className="text-muted-foreground hover:text-foreground transition-colors">
Privacy Policy
</Link>
</div>
<div className="space-y-5">
{sections.map((section, i) => (
<Card key={i}>
<CardHeader className="pb-3">
<CardTitle className="text-base">{section.title}</CardTitle>
</CardHeader>
<CardContent className="text-sm leading-relaxed text-muted-foreground">
{section.content}
</CardContent>
</Card>
))}
</div>
</main>
</div>
</div>
</Layout>
);
}