mirror of
https://github.com/layer-systems/website.git
synced 2026-09-13 14:14:07 +02:00
redesign as responsive Nostr OS
This commit is contained in:
@@ -6,6 +6,7 @@ import { Explore } from "./pages/Explore";
|
||||
import { Dashboard } from "./pages/Dashboard";
|
||||
import { DashboardEvents } from "./pages/DashboardEvents";
|
||||
import { DashboardExport } from "./pages/DashboardExport";
|
||||
import { Network } from "./pages/Network";
|
||||
import { NIP19Page } from "./pages/NIP19Page";
|
||||
import { Terms } from "./pages/Terms";
|
||||
import { Privacy } from "./pages/Privacy";
|
||||
@@ -21,6 +22,7 @@ export function AppRouter() {
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/dashboard/events" element={<DashboardEvents />} />
|
||||
<Route path="/dashboard/export" element={<DashboardExport />} />
|
||||
<Route path="/network" element={<Network />} />
|
||||
<Route path="/terms" element={<Terms />} />
|
||||
<Route path="/privacy" element={<Privacy />} />
|
||||
{/* NIP-19 route for npub1, note1, naddr1, nevent1, nprofile1 */}
|
||||
@@ -31,4 +33,4 @@ export function AppRouter() {
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
export default AppRouter;
|
||||
export default AppRouter;
|
||||
|
||||
107
src/components/navigation/OsShell.tsx
Normal file
107
src/components/navigation/OsShell.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Activity, Archive, Compass, Grid2X2, Network, Radio, Sparkles } from 'lucide-react';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const apps = [
|
||||
{ name: 'Home', path: '/', icon: Grid2X2, short: 'Home' },
|
||||
{ name: 'Discover', path: '/explore', icon: Compass, short: 'Find' },
|
||||
{ name: 'Activity', path: '/dashboard', icon: Activity, short: 'Me' },
|
||||
{ name: 'Studio', path: '/dashboard/events', icon: Sparkles, short: 'Studio' },
|
||||
{ name: 'Archive', path: '/dashboard/export', icon: Archive, short: 'Save' },
|
||||
{ name: 'Network', path: '/network', icon: Network, short: 'Relay' },
|
||||
];
|
||||
|
||||
interface OsShellProps {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
eyebrow?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function OsShell({ children, title, eyebrow = 'Nostr operating system', className }: OsShellProps) {
|
||||
return (
|
||||
<div className="os-shell min-h-dvh bg-background text-foreground">
|
||||
<aside className="os-rail fixed inset-y-0 left-0 z-30 hidden w-[232px] flex-col border-r border-white/10 px-3 py-4 lg:flex">
|
||||
<Link to="/" className="group mb-8 flex items-center gap-3 px-2" aria-label="Open Nostr OS home">
|
||||
<span className="grid h-10 w-10 place-items-center rounded-[14px] bg-primary text-primary-foreground shadow-[0_0_30px_hsl(var(--primary)/0.3)] transition-transform duration-300 group-hover:rotate-6">
|
||||
<Radio className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
<span>
|
||||
<span className="block font-mono text-[10px] uppercase tracking-[0.24em] text-primary">Layer systems</span>
|
||||
<span className="block text-lg font-semibold tracking-tight">Nostr / OS</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<p className="px-3 pb-3 font-mono text-[10px] uppercase tracking-[0.2em] text-muted-foreground">Applications</p>
|
||||
<nav className="space-y-1" aria-label="Applications">
|
||||
{apps.map((app) => (
|
||||
<NavLink
|
||||
key={app.path}
|
||||
to={app.path}
|
||||
end={app.path === '/'}
|
||||
className={({ isActive }) => cn(
|
||||
'group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm text-muted-foreground transition-all hover:bg-white/[0.06] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
isActive && 'bg-primary text-primary-foreground shadow-lg shadow-primary/20 hover:bg-primary hover:text-primary-foreground',
|
||||
)}
|
||||
>
|
||||
<app.icon className="h-[18px] w-[18px]" aria-hidden="true" />
|
||||
<span>{app.name}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="mt-auto rounded-2xl border border-white/10 bg-white/[0.035] p-3">
|
||||
<div className="mb-3 flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-60" />
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-400" />
|
||||
</span>
|
||||
Preferred relay
|
||||
</div>
|
||||
<Link to="/network" className="block truncate font-mono text-[11px] text-foreground hover:text-primary">
|
||||
relay.layer.systems
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="min-h-dvh pb-24 lg:ml-[232px] lg:pb-0">
|
||||
<header className="sticky top-0 z-20 border-b border-border/60 bg-background/80 px-4 py-3 backdrop-blur-xl sm:px-6 lg:px-8">
|
||||
<div className="mx-auto flex max-w-[1600px] items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<p className="font-mono text-[10px] uppercase tracking-[0.2em] text-primary">{eyebrow}</p>
|
||||
<h1 className="truncate text-lg font-semibold tracking-tight sm:text-xl">{title}</h1>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Link to="/network" className="hidden items-center gap-2 rounded-full border border-border/80 bg-card px-3 py-1.5 font-mono text-[10px] text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground sm:flex">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
|
||||
relay.layer.systems
|
||||
</Link>
|
||||
<LoginArea className="max-w-[220px]" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className={cn('mx-auto w-full max-w-[1600px] px-4 py-5 sm:px-6 sm:py-7 lg:px-8 lg:py-8', className)}>{children}</main>
|
||||
</div>
|
||||
|
||||
<nav className="fixed inset-x-3 bottom-3 z-30 grid grid-cols-6 rounded-2xl border border-white/10 bg-[hsl(220_18%_10%/0.92)] p-1.5 shadow-2xl shadow-black/30 backdrop-blur-xl lg:hidden" aria-label="Mobile applications">
|
||||
{apps.map((app) => (
|
||||
<NavLink
|
||||
key={app.path}
|
||||
to={app.path}
|
||||
end={app.path === '/'}
|
||||
className={({ isActive }) => cn(
|
||||
'flex min-w-0 flex-col items-center gap-1 rounded-xl px-1 py-2 font-mono text-[9px] uppercase tracking-tight text-slate-400 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
isActive && 'bg-primary text-primary-foreground',
|
||||
)}
|
||||
>
|
||||
<app.icon className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="truncate">{app.short}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -120,4 +120,62 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--radius: 0.875rem;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 320px;
|
||||
background:
|
||||
radial-gradient(circle at 88% -8%, hsl(var(--primary) / 0.09), transparent 30rem),
|
||||
hsl(var(--background));
|
||||
font-feature-settings: "ss01", "ss02", "cv02";
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.os-shell {
|
||||
background-image:
|
||||
linear-gradient(hsl(var(--foreground) / 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, hsl(var(--foreground) / 0.025) 1px, transparent 1px);
|
||||
background-size: 36px 36px;
|
||||
}
|
||||
|
||||
.os-rail {
|
||||
background:
|
||||
radial-gradient(circle at 30% 0%, hsl(var(--primary) / 0.12), transparent 14rem),
|
||||
hsl(220 18% 8% / 0.9);
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
|
||||
.os-hero {
|
||||
background:
|
||||
linear-gradient(120deg, hsl(222 28% 11% / 0.98), hsl(220 22% 15% / 0.96)),
|
||||
hsl(var(--card));
|
||||
}
|
||||
|
||||
.dark .os-shell {
|
||||
background-image:
|
||||
linear-gradient(hsl(0 0% 100% / 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, hsl(0 0% 100% / 0.025) 1px, transparent 1px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.os-hero {
|
||||
animation: os-rise 640ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes os-rise {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import { AppSidebar } from '@/components/navigation/AppSidebar';
|
||||
import { DashboardStats } from '@/components/dashboard/DashboardStats';
|
||||
import { EventKindsChart } from '@/components/dashboard/EventKindsChart';
|
||||
import { RecentActivityChart } from '@/components/dashboard/RecentActivityChart';
|
||||
@@ -10,29 +8,25 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { Account, useLoggedInAccounts } from '@/hooks/useLoggedInAccounts';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { OsShell } from '@/components/navigation/OsShell';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
|
||||
export function Dashboard() {
|
||||
const { user } = useCurrentUser();
|
||||
const { currentUser } = useLoggedInAccounts();
|
||||
|
||||
useSeoMeta({ title: 'Activity · Nostr OS', description: 'Your Nostr activity at a glance.' });
|
||||
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
|
||||
<OsShell title="Activity" eyebrow="Personal telemetry">
|
||||
<div className="space-y-6 overflow-x-hidden">
|
||||
{!user ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="py-12 px-8 text-center">
|
||||
<Card className="border-dashed bg-card/70">
|
||||
<CardContent className="py-16 px-8 text-center">
|
||||
<div className="max-w-sm mx-auto space-y-4">
|
||||
<Alert>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
@@ -45,14 +39,15 @@ export function Dashboard() {
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl md:text-3xl font-bold tracking-tight break-words">
|
||||
<section className="rounded-3xl border border-border/70 bg-card/70 p-5 sm:p-7">
|
||||
<p className="font-mono text-[10px] uppercase tracking-[0.2em] text-primary">Identity dashboard</p>
|
||||
<h2 className="mt-2 text-3xl md:text-4xl font-semibold tracking-tight break-words">
|
||||
Welcome back {currentUser ? getDisplayName(currentUser) : ''}!
|
||||
</h2>
|
||||
<p className="text-sm md:text-base text-muted-foreground">
|
||||
<p className="mt-3 text-sm md:text-base text-muted-foreground">
|
||||
Here's an overview of your Nostr activity and statistics.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<DashboardStats pubkey={user.pubkey} />
|
||||
|
||||
@@ -64,10 +59,8 @@ export function Dashboard() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</OsShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import { AppSidebar } from '@/components/navigation/AppSidebar';
|
||||
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 { EventExplorer } from '@/components/dashboard/EventExplorer';
|
||||
import { OsShell } from '@/components/navigation/OsShell';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
|
||||
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>
|
||||
</div>
|
||||
useSeoMeta({ title: 'Studio · Nostr OS', description: 'Browse and manage your published Nostr events.' });
|
||||
|
||||
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
|
||||
return (
|
||||
<OsShell title="Studio" eyebrow="Event workspace">
|
||||
<div className="space-y-6 overflow-x-hidden">
|
||||
{!user ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="py-12 px-8 text-center">
|
||||
<Card className="border-dashed bg-card/70">
|
||||
<CardContent className="py-16 px-8 text-center">
|
||||
<div className="max-w-sm mx-auto space-y-4">
|
||||
<Alert>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
@@ -35,23 +29,22 @@ 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">
|
||||
<section className="rounded-3xl border border-border/70 bg-card/70 p-5 sm:p-7">
|
||||
<p className="font-mono text-[10px] uppercase tracking-[0.2em] text-primary">Personal event index</p>
|
||||
<h2 className="mt-2 text-3xl md:text-4xl font-semibold tracking-tight break-words">
|
||||
Your Nostr events
|
||||
</h2>
|
||||
<p className="text-sm md:text-base text-muted-foreground max-w-2xl">
|
||||
<p className="mt-3 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>
|
||||
</section>
|
||||
|
||||
<EventExplorer pubkey={user.pubkey} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</OsShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import { AppSidebar } from '@/components/navigation/AppSidebar';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -9,12 +7,16 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { InfoIcon, Download, Users, Calendar } from 'lucide-react';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { OsShell } from '@/components/navigation/OsShell';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
|
||||
export function DashboardExport() {
|
||||
const { user } = useCurrentUser();
|
||||
const { nostr } = useNostr();
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
useSeoMeta({ title: 'Archive · Nostr OS', description: 'Export and preserve your Nostr following list.' });
|
||||
|
||||
// Fetch the user's contact list (kind 3 event)
|
||||
const { data: contactListEvent, isLoading } = useQuery({
|
||||
queryKey: ['contact-list', user?.pubkey],
|
||||
@@ -88,19 +90,11 @@ 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">
|
||||
<OsShell title="Archive" eyebrow="Portable identity">
|
||||
<div className="space-y-6 overflow-x-hidden">
|
||||
{!user ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="py-12 px-8 text-center">
|
||||
<Card className="border-dashed bg-card/70">
|
||||
<CardContent className="py-16 px-8 text-center">
|
||||
<div className="max-w-sm mx-auto space-y-4">
|
||||
<Alert>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
@@ -113,7 +107,12 @@ export function DashboardExport() {
|
||||
</Card>
|
||||
) : (
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
<Card>
|
||||
<section className="rounded-3xl border border-border/70 bg-card/70 p-5 sm:p-7">
|
||||
<p className="font-mono text-[10px] uppercase tracking-[0.2em] text-primary">Portable social graph</p>
|
||||
<h2 className="mt-2 text-3xl font-semibold tracking-tight">Keep your follows close.</h2>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">Your Nostr social graph belongs to you. Export a signed copy whenever you need it.</p>
|
||||
</section>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>Backup Your Following List</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -224,7 +223,7 @@ export function DashboardExport() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">About Your Following List</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -243,9 +242,7 @@ export function DashboardExport() {
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</OsShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { NoteContent } from '@/components/NoteContent';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { OsShell } from '@/components/navigation/OsShell';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
|
||||
function TextNoteCard({ event }: { event: NostrEvent }) {
|
||||
const author = useAuthor(event.pubkey);
|
||||
@@ -160,22 +162,22 @@ export function Explore() {
|
||||
const [activeTab, setActiveTab] = useState('notes');
|
||||
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">
|
||||
{/* 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
|
||||
</p>
|
||||
</div>
|
||||
useSeoMeta({
|
||||
title: 'Discover · Nostr OS',
|
||||
description: 'Explore notes and profiles through the Nostr network.',
|
||||
});
|
||||
|
||||
return (
|
||||
<OsShell title="Discover" eyebrow="Public network">
|
||||
<div className="mx-auto max-w-4xl space-y-6">
|
||||
<section className="overflow-hidden rounded-3xl border border-border/70 bg-card/70 p-5 sm:p-7">
|
||||
<p className="font-mono text-[10px] uppercase tracking-[0.2em] text-primary">Public network scan</p>
|
||||
<h2 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">Follow the live signal.</h2>
|
||||
<p className="mt-3 max-w-xl text-sm leading-6 text-muted-foreground sm:text-base">A quiet window into the notes and profiles moving through your configured Nostr relays.</p>
|
||||
</section>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 max-w-md mx-auto">
|
||||
<TabsList className="grid h-11 w-full grid-cols-2 rounded-xl border border-border bg-card p-1 sm:max-w-md">
|
||||
<TabsTrigger value="notes">
|
||||
Notes ({data?.textNotes?.length || 0})
|
||||
</TabsTrigger>
|
||||
@@ -184,7 +186,6 @@ export function Explore() {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Text Notes Tab */}
|
||||
<TabsContent value="notes" className="mt-6 space-y-4">
|
||||
{isLoading && <LoadingSkeleton />}
|
||||
|
||||
@@ -213,7 +214,6 @@ export function Explore() {
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
{/* Profiles Tab */}
|
||||
<TabsContent value="profiles" className="mt-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{isLoading && (
|
||||
@@ -265,6 +265,6 @@ export function Explore() {
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</OsShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,241 +1,87 @@
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { ArrowUpRight, Boxes, Copy, Fingerprint, Network, Orbit, Sparkles, Terminal, Waves } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useState } from '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 { OsShell } from '@/components/navigation/OsShell';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
|
||||
const workspaces = [
|
||||
{ title: 'Discover', detail: 'Live notes & people', path: '/explore', icon: Orbit, tone: 'from-cyan-400/20 to-blue-500/5', index: '01' },
|
||||
{ title: 'Activity', detail: 'Your Nostr footprint', path: '/dashboard', icon: Waves, tone: 'from-amber-300/20 to-orange-500/5', index: '02' },
|
||||
{ title: 'Studio', detail: 'Browse your events', path: '/dashboard/events', icon: Sparkles, tone: 'from-fuchsia-400/20 to-violet-500/5', index: '03' },
|
||||
{ title: 'Archive', detail: 'Export your follows', path: '/dashboard/export', icon: Boxes, tone: 'from-emerald-300/20 to-teal-500/5', index: '04' },
|
||||
];
|
||||
|
||||
const Index = () => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const { user } = useCurrentUser();
|
||||
const relayUrl = 'wss://relay.layer.systems';
|
||||
|
||||
useSeoMeta({
|
||||
title: 'LAYER.systems - Public Nostr Relay',
|
||||
description: 'A fast, reliable, and open Nostr relay serving the decentralized social network.',
|
||||
title: 'Nostr OS · Layer Systems',
|
||||
description: 'A beautiful operating system for your Nostr identity.',
|
||||
});
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
const copyRelay = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(relayUrl);
|
||||
setCopied(true);
|
||||
toast({
|
||||
title: 'Copied!',
|
||||
description: 'Relay URL copied to clipboard',
|
||||
});
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
window.setTimeout(() => setCopied(false), 1600);
|
||||
toast({ title: 'Relay address copied', description: 'Paste it into any Nostr client.' });
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Failed to copy',
|
||||
description: 'Please copy the URL manually',
|
||||
variant: 'destructive',
|
||||
});
|
||||
toast({ title: 'Could not copy relay address', description: relayUrl, variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Gift,
|
||||
title: 'Free',
|
||||
description: 'No cost to use - accessible for everyone',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Community driven',
|
||||
description: 'Built and maintained by the Nostr community',
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
title: 'Global Network',
|
||||
description: 'Part of the decentralized Nostr ecosystem',
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
title: 'Open Access',
|
||||
description: 'Free to use for all Nostr clients',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-background via-background to-muted/20">
|
||||
{/* Header */}
|
||||
<header className="absolute top-0 left-0 right-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex justify-end">
|
||||
<LoginArea className="max-w-60" />
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Add this URL to your Nostr client to connect to LAYER.systems
|
||||
</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>
|
||||
))}
|
||||
<OsShell title="Home" eyebrow="Layer Systems / Nostr OS">
|
||||
<section className="os-hero relative isolate overflow-hidden rounded-[28px] border border-white/10 px-5 py-7 shadow-2xl shadow-black/10 sm:px-8 sm:py-10 lg:px-10 lg:py-12">
|
||||
<div className="absolute -right-16 -top-16 h-64 w-64 rounded-full bg-primary/20 blur-3xl" />
|
||||
<div className="absolute -bottom-24 left-1/3 h-56 w-56 rounded-full bg-cyan-400/10 blur-3xl" />
|
||||
<div className="relative grid gap-8 lg:grid-cols-[1.35fr_.65fr] lg:items-end">
|
||||
<div>
|
||||
<div className="mb-5 inline-flex items-center gap-2 rounded-full border border-white/10 bg-black/15 px-3 py-1.5 font-mono text-[10px] uppercase tracking-[0.18em] text-slate-300">
|
||||
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-400" /> Personal signal online
|
||||
</div>
|
||||
<p className="max-w-xl text-balance text-4xl font-semibold leading-[0.98] tracking-[-0.055em] text-white sm:text-5xl lg:text-6xl">One place for the network that belongs to you.</p>
|
||||
<p className="mt-5 max-w-lg text-pretty text-sm leading-6 text-slate-300 sm:text-base">A calm control surface for your Nostr identity: discover public conversations, inspect your signal, and keep your network portable.</p>
|
||||
<div className="mt-7 flex flex-wrap gap-3">
|
||||
<Link to={user ? '/dashboard' : '/explore'} className="inline-flex items-center gap-2 rounded-xl bg-primary px-4 py-3 text-sm font-semibold text-primary-foreground shadow-lg shadow-primary/20 transition-transform hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white">
|
||||
{user ? 'Open my activity' : 'Enter discover'} <ArrowUpRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link to="/network" className="inline-flex items-center gap-2 rounded-xl border border-white/15 bg-white/[0.06] px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-white/[0.12] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white">
|
||||
<Network className="h-4 w-4" /> Network routes
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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 className="relative overflow-hidden rounded-2xl border border-white/10 bg-black/20 p-5 backdrop-blur-sm">
|
||||
<div className="mb-8 flex items-center justify-between font-mono text-[10px] uppercase tracking-[0.16em] text-slate-400"><span>Identity</span><Fingerprint className="h-4 w-4 text-primary" /></div>
|
||||
<p className="text-lg font-medium text-white">{user ? 'Signer connected' : 'Your Nostr key is waiting'}</p>
|
||||
<p className="mt-2 font-mono text-xs leading-5 text-slate-400">{user ? `${user.pubkey.slice(0, 12)}…${user.pubkey.slice(-8)}` : 'Use a signer extension or nsec login to begin.'}</p>
|
||||
<div className="mt-7 flex items-center gap-2 text-xs text-emerald-300"><span className="h-2 w-2 rounded-full bg-emerald-400" /> Your identity never leaves your signer</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 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>
|
||||
<section className="mt-8">
|
||||
<div className="mb-4 flex items-end justify-between gap-4"><div><p className="font-mono text-[10px] uppercase tracking-[0.2em] text-primary">Your desktop</p><h2 className="mt-1 text-2xl font-semibold tracking-tight">Open an app</h2></div><span className="hidden font-mono text-[10px] text-muted-foreground sm:block">04 MODULES READY</span></div>
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{workspaces.map((workspace) => (
|
||||
<Link key={workspace.path} to={workspace.path} className="group relative min-h-[178px] overflow-hidden rounded-2xl border border-border/70 bg-card/80 p-5 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-primary/40 hover:shadow-xl hover:shadow-primary/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary">
|
||||
<div className={`absolute inset-0 bg-gradient-to-br ${workspace.tone} opacity-0 transition-opacity duration-300 group-hover:opacity-100`} />
|
||||
<div className="relative flex h-full flex-col"><div className="flex items-start justify-between"><span className="grid h-10 w-10 place-items-center rounded-xl border border-border bg-background/80"><workspace.icon className="h-5 w-5 text-primary" /></span><span className="font-mono text-[10px] text-muted-foreground">{workspace.index}</span></div><div className="mt-auto"><h3 className="font-semibold">{workspace.title}</h3><p className="mt-1 text-sm text-muted-foreground">{workspace.detail}</p></div></div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-8 grid gap-4 lg:grid-cols-[1.35fr_.65fr]">
|
||||
<div className="rounded-2xl border border-border/70 bg-card/70 p-5 sm:p-6"><div className="flex items-center gap-3"><span className="grid h-10 w-10 place-items-center rounded-xl bg-primary/10 text-primary"><Terminal className="h-5 w-5" /></span><div><h2 className="font-semibold">Bring Layer Systems with you</h2><p className="text-sm text-muted-foreground">A preferred relay, ready for any Nostr client.</p></div></div><div className="mt-5 flex flex-col gap-3 rounded-xl border border-border bg-background/60 p-3 sm:flex-row sm:items-center sm:justify-between"><code className="min-w-0 truncate font-mono text-sm text-foreground">{relayUrl}</code><button type="button" onClick={copyRelay} className="inline-flex shrink-0 items-center justify-center gap-2 rounded-lg bg-secondary px-3 py-2 text-xs font-semibold transition-colors hover:bg-primary hover:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"><Copy className="h-3.5 w-3.5" />{copied ? 'Copied' : 'Copy relay'}</button></div></div>
|
||||
<Link to="/network" className="group rounded-2xl border border-border/70 bg-card/70 p-5 transition-colors hover:border-primary/40 sm:p-6"><p className="font-mono text-[10px] uppercase tracking-[0.18em] text-primary">Your route table</p><p className="mt-3 text-lg font-semibold">Control what carries your signal.</p><p className="mt-2 text-sm leading-6 text-muted-foreground">Manage read and write relays from one focused network app.</p><span className="mt-5 inline-flex items-center gap-2 text-sm font-medium text-primary">Open Network <ArrowUpRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" /></span></Link>
|
||||
</section>
|
||||
</OsShell>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,42 +1,55 @@
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { Copy, Fingerprint, MapPin, Radio } from 'lucide-react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { OsShell } from '@/components/navigation/OsShell';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import NotFound from './NotFound';
|
||||
|
||||
const labels: Record<string, { title: string; detail: string }> = {
|
||||
npub: { title: 'Profile pointer', detail: 'A public key that identifies a person or service on Nostr.' },
|
||||
nprofile: { title: 'Profile pointer', detail: 'A profile pointer with relay hints for faster discovery.' },
|
||||
note: { title: 'Note pointer', detail: 'A pointer to a short text note on the Nostr network.' },
|
||||
nevent: { title: 'Event pointer', detail: 'A portable pointer to a Nostr event and its relay hints.' },
|
||||
naddr: { title: 'Address pointer', detail: 'A durable address for a replaceable Nostr event.' },
|
||||
};
|
||||
|
||||
export function NIP19Page() {
|
||||
const { nip19: identifier } = useParams<{ nip19: string }>();
|
||||
const { toast } = useToast();
|
||||
|
||||
if (!identifier) {
|
||||
return <NotFound />;
|
||||
}
|
||||
if (!identifier) return <NotFound />;
|
||||
|
||||
let decoded;
|
||||
let decoded: ReturnType<typeof nip19.decode>;
|
||||
try {
|
||||
decoded = nip19.decode(identifier);
|
||||
} catch {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
const { type } = decoded;
|
||||
const presentation = labels[decoded.type];
|
||||
if (!presentation) return <NotFound />;
|
||||
|
||||
switch (type) {
|
||||
case 'npub':
|
||||
case 'nprofile':
|
||||
// AI agent should implement profile view here
|
||||
return <div>Profile placeholder</div>;
|
||||
const detail = typeof decoded.data === 'string' ? decoded.data : JSON.stringify(decoded.data, null, 2);
|
||||
|
||||
case 'note':
|
||||
// AI agent should implement note view here
|
||||
return <div>Note placeholder</div>;
|
||||
const copyPointer = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(identifier);
|
||||
toast({ title: 'Nostr pointer copied' });
|
||||
} catch {
|
||||
toast({ title: 'Could not copy pointer', variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
case 'nevent':
|
||||
// AI agent should implement event view here
|
||||
return <div>Event placeholder</div>;
|
||||
|
||||
case 'naddr':
|
||||
// AI agent should implement addressable event view here
|
||||
return <div>Addressable event placeholder</div>;
|
||||
|
||||
default:
|
||||
return <NotFound />;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<OsShell title="Pointer" eyebrow="NIP-19 resolver">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<section className="rounded-3xl border border-primary/25 bg-primary/[0.07] p-6 sm:p-8">
|
||||
<div className="flex items-start gap-4"><span className="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-primary text-primary-foreground"><Fingerprint className="h-5 w-5" /></span><div><p className="font-mono text-[10px] uppercase tracking-[0.2em] text-primary">Resolved {decoded.type}</p><h2 className="mt-1 text-3xl font-semibold tracking-tight">{presentation.title}</h2><p className="mt-3 max-w-xl text-sm leading-6 text-muted-foreground">{presentation.detail}</p></div></div>
|
||||
</section>
|
||||
<Card className="mt-5 border-border/70 bg-card/70"><CardContent className="p-5 sm:p-6"><div className="flex items-center justify-between gap-3"><div className="flex items-center gap-2 text-sm font-semibold"><Radio className="h-4 w-4 text-primary" /> Nostr address</div><button type="button" onClick={copyPointer} className="inline-flex items-center gap-2 rounded-lg bg-secondary px-3 py-2 text-xs font-semibold transition-colors hover:bg-primary hover:text-primary-foreground"><Copy className="h-3.5 w-3.5" /> Copy</button></div><code className="mt-4 block overflow-x-auto rounded-xl border border-border bg-background/60 p-4 font-mono text-xs leading-6 text-muted-foreground">{identifier}</code></CardContent></Card>
|
||||
<Card className="mt-5 border-border/70 bg-card/70"><CardContent className="p-5 sm:p-6"><div className="flex items-center gap-2 text-sm font-semibold"><MapPin className="h-4 w-4 text-primary" /> Decoded route</div><pre className="mt-4 overflow-x-auto rounded-xl border border-border bg-background/60 p-4 font-mono text-xs leading-6 text-muted-foreground">{detail}</pre></CardContent></Card>
|
||||
</div>
|
||||
</OsShell>
|
||||
);
|
||||
}
|
||||
|
||||
59
src/pages/Network.tsx
Normal file
59
src/pages/Network.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { ExternalLink, Radio, ShieldCheck, Zap } from 'lucide-react';
|
||||
import { OsShell } from '@/components/navigation/OsShell';
|
||||
import { RelayListManager } from '@/components/RelayListManager';
|
||||
|
||||
export function Network() {
|
||||
useSeoMeta({
|
||||
title: 'Network · Nostr OS',
|
||||
description: 'Manage the relays that connect your Nostr OS.',
|
||||
});
|
||||
|
||||
return (
|
||||
<OsShell title="Network" eyebrow="Connection control">
|
||||
<section className="mb-7 overflow-hidden rounded-3xl border border-primary/25 bg-primary/[0.07] p-5 shadow-[0_20px_70px_hsl(var(--primary)/0.08)] sm:p-7">
|
||||
<div className="flex flex-col justify-between gap-5 md:flex-row md:items-end">
|
||||
<div className="max-w-2xl">
|
||||
<div className="mb-4 flex h-11 w-11 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-lg shadow-primary/20">
|
||||
<Radio className="h-5 w-5" />
|
||||
</div>
|
||||
<p className="font-mono text-[10px] uppercase tracking-[0.22em] text-primary">Layer systems relay</p>
|
||||
<h2 className="mt-1 text-3xl font-semibold tracking-tight sm:text-4xl">Your signal starts here.</h2>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground sm:text-base">Nostr OS ships with Layer Systems as its preferred relay. Keep it enabled for a fast, simple default, then add the relays your identity already trusts.</p>
|
||||
</div>
|
||||
<a href="https://relay.layer.systems" target="_blank" rel="noreferrer" className="inline-flex items-center gap-2 self-start rounded-xl border border-border bg-card px-4 py-2.5 text-sm font-medium transition-colors hover:border-primary/50 hover:text-primary md:self-auto">
|
||||
Visit relay <ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_300px]">
|
||||
<section className="rounded-3xl border border-border/70 bg-card/70 p-5 shadow-sm sm:p-7">
|
||||
<div className="mb-6 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Relay routes</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">Read, write, and publish your NIP-65 relay preferences.</p>
|
||||
</div>
|
||||
<span className="flex shrink-0 items-center gap-2 rounded-full bg-emerald-400/10 px-3 py-1 font-mono text-[10px] uppercase tracking-wider text-emerald-500"><span className="h-1.5 w-1.5 rounded-full bg-emerald-400" /> Ready</span>
|
||||
</div>
|
||||
<RelayListManager />
|
||||
</section>
|
||||
|
||||
<aside className="grid gap-4 sm:grid-cols-2 xl:grid-cols-1">
|
||||
<div className="rounded-3xl border border-border/70 bg-card/70 p-5">
|
||||
<ShieldCheck className="mb-4 h-5 w-5 text-primary" />
|
||||
<h3 className="font-semibold">Your keys stay yours</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">Login uses your Nostr signer. Relay preferences are only published after you choose to change them.</p>
|
||||
</div>
|
||||
<div className="rounded-3xl border border-border/70 bg-card/70 p-5">
|
||||
<Zap className="mb-4 h-5 w-5 text-primary" />
|
||||
<h3 className="font-semibold">One clean default</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">Start with relay.layer.systems, then expand your route table whenever you need reach or redundancy.</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</OsShell>
|
||||
);
|
||||
}
|
||||
|
||||
export default Network;
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useSeoMeta } from "@unhead/react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowLeft, Radar } from 'lucide-react';
|
||||
import { OsShell } from '@/components/navigation/OsShell';
|
||||
|
||||
const NotFound = () => {
|
||||
const location = useLocation();
|
||||
@@ -18,15 +21,17 @@ 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>
|
||||
<OsShell title="Lost signal" eyebrow="404">
|
||||
<div className="grid min-h-[60vh] place-items-center">
|
||||
<div className="max-w-md text-center">
|
||||
<div className="mx-auto mb-5 grid h-16 w-16 place-items-center rounded-3xl bg-primary/10 text-primary"><Radar className="h-7 w-7" /></div>
|
||||
<p className="font-mono text-xs tracking-[0.22em] text-primary">404 / NO ROUTE</p>
|
||||
<h2 className="mt-3 text-4xl font-semibold tracking-tight">That signal isn’t here.</h2>
|
||||
<p className="mt-4 text-muted-foreground">The route may have moved, expired, or never existed in this Nostr OS.</p>
|
||||
<Link to="/" className="mt-7 inline-flex items-center gap-2 rounded-xl bg-primary px-4 py-3 text-sm font-semibold text-primary-foreground"><ArrowLeft className="h-4 w-4" /> Return home</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OsShell>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 { OsShell } from '@/components/navigation/OsShell';
|
||||
|
||||
export function Privacy() {
|
||||
useSeoMeta({
|
||||
@@ -10,19 +11,8 @@ export function Privacy() {
|
||||
});
|
||||
|
||||
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">
|
||||
<OsShell title="Privacy" eyebrow="Layer Systems">
|
||||
<main className="mx-auto max-w-4xl">
|
||||
<div className="space-y-8">
|
||||
{/* Hero Section */}
|
||||
<div className="text-center space-y-4 pb-8">
|
||||
@@ -36,7 +26,7 @@ export function Privacy() {
|
||||
</div>
|
||||
|
||||
{/* Introduction */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>Introduction</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -48,7 +38,7 @@ export function Privacy() {
|
||||
</Card>
|
||||
|
||||
{/* Information We Collect */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>1. Information We Collect</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -88,7 +78,7 @@ export function Privacy() {
|
||||
</Card>
|
||||
|
||||
{/* How We Use Information */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>2. How We Use Information</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -107,7 +97,7 @@ export function Privacy() {
|
||||
</Card>
|
||||
|
||||
{/* Data Storage and Security */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>3. Data Storage and Security</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -131,7 +121,7 @@ export function Privacy() {
|
||||
</Card>
|
||||
|
||||
{/* Data Retention */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>4. Data Retention</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -152,7 +142,7 @@ export function Privacy() {
|
||||
</Card>
|
||||
|
||||
{/* Third-Party Services */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>5. Third-Party Services</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -173,7 +163,7 @@ export function Privacy() {
|
||||
</Card>
|
||||
|
||||
{/* Your Rights */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>6. Your Rights and Choices</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -191,7 +181,7 @@ export function Privacy() {
|
||||
</Card>
|
||||
|
||||
{/* Cookies and Tracking */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>7. Cookies and Tracking</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -211,7 +201,7 @@ export function Privacy() {
|
||||
</Card>
|
||||
|
||||
{/* Children's Privacy */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>8. Children's Privacy</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -223,7 +213,7 @@ export function Privacy() {
|
||||
</Card>
|
||||
|
||||
{/* International Users */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>9. International Users</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -235,7 +225,7 @@ export function Privacy() {
|
||||
</Card>
|
||||
|
||||
{/* Changes to Privacy Policy */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>10. Changes to This Privacy Policy</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -271,7 +261,7 @@ export function Privacy() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</OsShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 { OsShell } from '@/components/navigation/OsShell';
|
||||
|
||||
export function Terms() {
|
||||
useSeoMeta({
|
||||
@@ -10,19 +11,8 @@ export function Terms() {
|
||||
});
|
||||
|
||||
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">
|
||||
<OsShell title="Terms" eyebrow="Layer Systems">
|
||||
<main className="mx-auto max-w-4xl">
|
||||
<div className="space-y-8">
|
||||
{/* Hero Section */}
|
||||
<div className="text-center space-y-4 pb-8">
|
||||
@@ -36,7 +26,7 @@ export function Terms() {
|
||||
</div>
|
||||
|
||||
{/* Introduction */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>Introduction</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -48,7 +38,7 @@ export function Terms() {
|
||||
</Card>
|
||||
|
||||
{/* Service Description */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>1. Service Description</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -65,7 +55,7 @@ export function Terms() {
|
||||
</Card>
|
||||
|
||||
{/* User Conduct */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>2. User Conduct</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -88,7 +78,7 @@ export function Terms() {
|
||||
</Card>
|
||||
|
||||
{/* Content Policy */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>3. Content Policy</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -109,7 +99,7 @@ export function Terms() {
|
||||
</Card>
|
||||
|
||||
{/* Limitation of Liability */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>4. Limitation of Liability</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -131,7 +121,7 @@ export function Terms() {
|
||||
</Card>
|
||||
|
||||
{/* Privacy */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>5. Privacy</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -147,7 +137,7 @@ export function Terms() {
|
||||
</Card>
|
||||
|
||||
{/* Changes to Terms */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>6. Changes to Terms</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -159,7 +149,7 @@ export function Terms() {
|
||||
</Card>
|
||||
|
||||
{/* Termination */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>7. Termination</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -171,7 +161,7 @@ export function Terms() {
|
||||
</Card>
|
||||
|
||||
{/* Governing Law */}
|
||||
<Card>
|
||||
<Card className="border-border/70 bg-card/70">
|
||||
<CardHeader>
|
||||
<CardTitle>8. Governing Law</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -207,7 +197,7 @@ export function Terms() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</OsShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user