From 98778e77ba9b763358c52b4d28cd7c73df91c76d Mon Sep 17 00:00:00 2001 From: highperfocused Date: Sat, 28 Mar 2026 21:43:43 +0100 Subject: [PATCH] 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. --- package-lock.json | 57 ++- package.json | 2 + src/components/DashboardLayout.tsx | 44 +++ src/components/Layout.tsx | 151 ++++++++ src/components/navigation/AppSidebar.tsx | 25 +- src/index.css | 219 ++++++----- src/main.tsx | 4 + src/pages/Dashboard.tsx | 79 ++-- src/pages/DashboardEvents.tsx | 71 ++-- src/pages/DashboardExport.tsx | 291 +++++++-------- src/pages/Explore.tsx | 138 ++++--- src/pages/Index.tsx | 318 ++++++++-------- src/pages/NIP19Page.tsx | 100 ++++- src/pages/NotFound.tsx | 29 +- src/pages/Privacy.tsx | 454 ++++++++++------------- src/pages/Terms.tsx | 338 +++++++---------- tailwind.config.ts | 4 + 17 files changed, 1252 insertions(+), 1072 deletions(-) create mode 100644 src/components/DashboardLayout.tsx create mode 100644 src/components/Layout.tsx diff --git a/package-lock.json b/package-lock.json index 2b607ec..98960f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "website", "version": "0.0.0", "dependencies": { + "@fontsource-variable/bitter": "^5.2.10", "@fontsource-variable/inter": "^5.2.6", + "@fontsource-variable/outfit": "^5.2.8", "@getalby/sdk": "^5.1.1", "@hookform/resolvers": "^3.9.0", "@nostrify/nostrify": "^0.48.2", @@ -133,7 +135,6 @@ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", @@ -302,6 +303,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -325,6 +327,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -946,6 +949,15 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@fontsource-variable/bitter": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@fontsource-variable/bitter/-/bitter-5.2.10.tgz", + "integrity": "sha512-OGvp+KEyqOq+4/20Y9vifBxSBnfxGiL/9qiSl9rOMZSRhEEbgAYBn+DKVPXIWSQQdawcYc975qE13ppRIcPI2Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@fontsource-variable/inter": { "version": "5.2.6", "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.6.tgz", @@ -955,6 +967,15 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@fontsource-variable/outfit": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/outfit/-/outfit-5.2.8.tgz", + "integrity": "sha512-4oUDCZx/Tcz6HZP423w/niqEH31Gks5IsqHV2ZZz1qKHaVIZdj2f0/S1IK2n8jl6Xo0o3N+3RjNHlV9R73ozQA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@getalby/lightning-tools": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@getalby/lightning-tools/-/lightning-tools-5.2.0.tgz", @@ -4012,8 +4033,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/d3-array": { "version": "3.2.1", @@ -4124,6 +4144,7 @@ "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4135,6 +4156,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4175,6 +4197,7 @@ "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.31.1", "@typescript-eslint/types": "8.31.1", @@ -4520,6 +4543,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4790,6 +4814,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -5302,6 +5327,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -5403,8 +5429,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -5433,7 +5458,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -5560,6 +5586,7 @@ "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -6336,6 +6363,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -6528,7 +6556,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6968,6 +6995,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -7122,7 +7150,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -7138,7 +7165,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -7149,7 +7175,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -7162,8 +7187,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/prop-types": { "version": "15.8.1", @@ -7234,6 +7258,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7260,6 +7285,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -7273,6 +7299,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.1.tgz", "integrity": "sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -7575,6 +7602,7 @@ "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -7941,6 +7969,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -8074,6 +8103,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8213,6 +8243,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8453,6 +8484,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -8566,6 +8598,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 5dc7498..9384825 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "deploy": "npm run build && npx -y nostr-deploy-cli deploy --skip-setup" }, "dependencies": { + "@fontsource-variable/bitter": "^5.2.10", "@fontsource-variable/inter": "^5.2.6", + "@fontsource-variable/outfit": "^5.2.8", "@getalby/sdk": "^5.1.1", "@hookform/resolvers": "^3.9.0", "@nostrify/nostrify": "^0.48.2", diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx new file mode 100644 index 0000000..d34666a --- /dev/null +++ b/src/components/DashboardLayout.tsx @@ -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 ( + +
+ +
+ {/* Top bar */} +
+ +

+ {title} +

+
+ + + Home + + +
+
+ + {/* Page content */} +
+ {children} +
+
+
+
+ ); +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..dc0a125 --- /dev/null +++ b/src/components/Layout.tsx @@ -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 ( +
+ {/* ─── Header ─── */} +
+
+ {/* Logo */} + +
+ +
+ LAYER.systems + + + {/* Desktop nav */} + + + {/* Right side: login + mobile burger */} +
+ + +
+
+ + {/* Mobile nav panel */} + {mobileOpen && ( +
+ +
+ +
+
+ )} +
+ + {/* ─── Main content ─── */} +
+ {children} +
+ + {/* ─── Footer ─── */} + {!hideFooter && ( +
+
+
+
+
+ +
+ © {new Date().getFullYear()} LAYER.systems +
+
+ + Explore + + + Terms + + + Privacy + +
+
+
+
+ )} +
+ ); +} diff --git a/src/components/navigation/AppSidebar.tsx b/src/components/navigation/AppSidebar.tsx index 79481d4..b6f07c2 100644 --- a/src/components/navigation/AppSidebar.tsx +++ b/src/components/navigation/AppSidebar.tsx @@ -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 ( + {/* Brand header */} + + +
+ +
+ + LAYER.systems + + +
+ - Navigation + + Navigation + {navigationItems.map((item) => { @@ -52,7 +70,7 @@ export function AppSidebar() { - + {item.title} @@ -63,6 +81,7 @@ export function AppSidebar() { +
diff --git a/src/index.css b/src/index.css index 78a8649..2b1a242 100644 --- a/src/index.css +++ b/src/index.css @@ -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); } -} \ No newline at end of file + + 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); + } +} diff --git a/src/main.tsx b/src/main.tsx index 730e0ee..75cdf8c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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'; diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 171e108..f89573e 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -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 ( - -
- -
-
- -

Dashboard

+ + {!user ? ( + + +
+ + + + Please log in to view your dashboard and activity statistics. + + +
+
+
+ ) : ( + <> +
+

+ Welcome back{currentUser ? `, ${getDisplayName(currentUser)}` : ''} +

+

+ Here's an overview of your Nostr activity and statistics. +

- -
- {!user ? ( - - -
- - - - Please log in to view your dashboard and activity statistics. - - -
-
-
- ) : ( - <> -
-

- Welcome back {currentUser ? getDisplayName(currentUser) : ''}! -

-

- Here's an overview of your Nostr activity and statistics. -

-
- + - + -
- - -
- - )} +
+ +
-
-
-
+ + )} + ); } diff --git a/src/pages/DashboardEvents.tsx b/src/pages/DashboardEvents.tsx index 4fe2d21..716c288 100644 --- a/src/pages/DashboardEvents.tsx +++ b/src/pages/DashboardEvents.tsx @@ -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 ( - -
- -
-
- -

My Events

+ + {!user ? ( + + +
+ + + + Please log in to explore your events and manage deletion requests. + + +
+
+
+ ) : ( +
+
+

+ Your Nostr Events +

+

+ Browse all events you have published on Nostr, search through their content, + and publish deletion requests when you want something removed. +

-
- {!user ? ( - - -
- - - - Please log in to explore your events and manage deletion requests. - - -
-
-
- ) : ( -
-
-

- Your Nostr events -

-

- Browse all events you have published on Nostr, search through their content, - and publish deletion requests when you want something removed. -

-
- - -
- )} -
-
-
-
+ +
+ )} + ); } diff --git a/src/pages/DashboardExport.tsx b/src/pages/DashboardExport.tsx index 0255a8c..529bd78 100644 --- a/src/pages/DashboardExport.tsx +++ b/src/pages/DashboardExport.tsx @@ -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 ( - -
- -
-
- -

Export Following List

-
- -
- {!user ? ( - - -
- - - - Please log in to export your following list. - - + + {!user ? ( + + +
+ + + + Please log in to export your following list. + + +
+
+
+ ) : ( +
+ + + Backup Your Following List + + Export your contact list as a JSON file for backup or migration purposes. + + + + {isLoading ? ( +
+
+ +
+ + +
- - - ) : ( -
- - - Backup Your Following List - - Export your contact list as a JSON file for backup or migration purposes. - - - - {isLoading ? ( -
+
+ +
+ + +
+
+
+ ) : !contactListEvent ? ( + + + + No following list found. You may need to follow some users first. + + + ) : ( + <> +
+ +
- -
- - +
+ +
+
+

Following

+

{followingCount}

+ + + + +
- -
- - +
+ +
+
+

Last Updated

+

+ {createdDate + ? createdDate.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }) + : 'Unknown'} +

+

+ {createdDate + ? createdDate.toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit', + }) + : ''} +

-
- ) : !contactListEvent ? ( - - - - No following list found. You may need to follow some users first. - - - ) : ( - <> -
- - -
-
- -
-
-

Following

-

{followingCount}

-
-
-
-
+ + +
- - -
-
- -
-
-

Last Updated

-

- {createdDate - ? createdDate.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - }) - : 'Unknown'} -

-

- {createdDate - ? createdDate.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - }) - : ''} -

-
-
-
-
-
+
+
+

What's included:

+
    +
  • Complete list of all {followingCount} accounts you follow
  • +
  • Public keys (pubkeys) for each account
  • +
  • Relay hints and petnames (if available)
  • +
  • Original event data and signature
  • +
  • Timestamp of when the list was created
  • +
+
-
-
-

What's included:

-
    -
  • • Complete list of all {followingCount} accounts you follow
  • -
  • • Public keys (pubkeys) for each account
  • -
  • • Relay hints and petnames (if available)
  • -
  • • Original event data and signature
  • -
  • • Timestamp of when the list was created
  • -
-
+ - - -

- Your backup file will be saved to your downloads folder -

-
- - )} - - - - - - About Your Following List - - -

- 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. +

+ Your backup file will be saved to your downloads folder

-

- 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. -

-
-
-
- )} -
-
-
-
+ + + )} + + + + + + About Your Following List + + +

+ 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. +

+

+ 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. +

+
+
+ + )} + ); } diff --git a/src/pages/Explore.tsx b/src/pages/Explore.tsx index 0250d76..90eaf84 100644 --- a/src/pages/Explore.tsx +++ b/src/pages/Explore.tsx @@ -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 ( - + -
- +
+ - {displayName[0]?.toUpperCase()} + {displayName[0]?.toUpperCase()} -
-
-

{displayName}

+
+
+ {displayName} {metadata?.nip05 && ( - - ✓ - + NIP-05 )}
-

@{username}

+

@{username}

- +
@@ -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 ( - - {banner && ( -
- + + {banner ? ( +
+
+ ) : ( +
)} - -
- + +
+ - {displayName[0]?.toUpperCase()} + {displayName[0]?.toUpperCase()} -
-
-

{displayName}

+
+
+

{displayName}

{nip05 && ( - - ✓ - + NIP-05 )}
-

@{username}

- {nip05 && ( -

{nip05}

- )} +

@{username}

{(about || website) && ( - + {about && ( -

{about}

+

{about}

)} {website && ( - 🔗 {website.replace(/^https?:\/\//, '')} + {website.replace(/^https?:\/\//, '')} )}
@@ -135,11 +125,11 @@ function LoadingSkeleton() { {[1, 2, 3].map((i) => ( -
- -
- - +
+ +
+ +
@@ -161,21 +151,19 @@ export function Explore() { const { data, isLoading, isError } = useExploreEvents(); return ( -
-
+ +
{/* Header */} -
-

- Explore -

-

- Discover the latest text notes and profiles from the Nostr +

+

Explore

+

+ The latest notes and profiles from the network.

{/* Tabs */} - - + + Notes ({data?.textNotes?.length || 0}) @@ -184,15 +172,15 @@ export function Explore() { - {/* Text Notes Tab */} - + {/* Notes Tab */} + {isLoading && } - + {isError && ( - +

- Unable to load notes. Please check your relay connections. + Unable to load notes. Check your relay connections.

@@ -200,9 +188,9 @@ export function Explore() { {!isLoading && !isError && data?.textNotes.length === 0 && ( - +

- No text notes found. Try refreshing or check your relay connections. + No notes found. Try refreshing or check your relay connections.

@@ -214,18 +202,18 @@ export function Explore() {
{/* Profiles Tab */} - -
+ +
{isLoading && ( <> {[1, 2, 3, 4].map((i) => ( -
- -
- - +
+ +
+ +
@@ -237,9 +225,9 @@ export function Explore() { {isError && (
- +

- Unable to load profiles. Please check your relay connections. + Unable to load profiles. Check your relay connections.

@@ -249,9 +237,9 @@ export function Explore() { {!isLoading && !isError && data?.profiles.length === 0 && (
- +

- No profiles found. Try refreshing or check your relay connections. + No profiles found.

@@ -265,6 +253,6 @@ export function Explore() {
-
+ ); } diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index fb16a26..da93fc2 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -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 ( -
- {/* Header */} -
-
-
- -
-
-
- - {/* Hero Section */} -
- {/* Animated Background Elements */} -
-
-
+ + {/* ─── Hero ─── */} +
+ {/* Abstract background shapes */} +
+
+
-
- {/* Status Badge */} - {/*
- -
- Relay Online - -
*/} - - {/* Main Heading */} -
-

- - LAYER.systems +
+ {/* Badge */} +
+ + + + + Relay online + +
+ + {/* Headline */} +
+

+ Your gateway to the{' '} + open social web

-

- Your gateway to the decentralized social network -

-

- A fast, reliable, and open Nostr relay connecting you to the future of social media +

+ A fast, reliable Nostr relay connecting you to the future of decentralized social media.

- {/* Relay URL Card */} -
- - -
-
-
-

Relay URL

- - {relayUrl} - -
- + {/* Relay URL card */} +
+ + +

+ Relay URL +

+
+ + {relayUrl} + + +
+
+
+
+ + {/* CTAs */} +
+ + +
+
+

+ + {/* ─── Features ─── */} +
+
+
+

Why LAYER.systems?

+

+ Infrastructure you can rely on, built for the decentralized future. +

+
+ +
+ {features.map((feature, i) => ( + + +
+
-

- Add this URL to your Nostr client to connect to LAYER.systems +

{feature.title}

+

+ {feature.description}

-
- - -
- - {/* Features Grid */} -
-

Why Choose LAYER.systems?

-
- {features.map((feature, index) => ( - - -
- -
-
-

{feature.title}

-

{feature.description}

-
-
-
- ))} -
+ + + ))}
-
+ - {/* How to Connect Section */} -
-
-

Getting Started

-
- - -
-
-
- 1 -
-
-

Choose Your Client

-

- Pick a Nostr client like Damus, Amethyst, Snort, or any other compatible application -

-
-
-
-
- 2 -
-
-

Add the Relay

-

- In your client settings, add {relayUrl} to your relay list -

-
-
-
-
- 3 -
-
-

Start Connecting

-

- You're all set! Start posting, following, and connecting with the Nostr network -

-
-
-
-
-
-
-
-
- - {/* Footer */} -
-
-
-

- © {new Date().getFullYear()} LAYER.systems. Powered by Nostr. + {/* ─── Getting Started ─── */} +

+
+
+

Get started in minutes

+

+ Three steps to join the Nostr network through our relay.

-
-
- - Open and free for all +
+ +
+ {steps.map((step) => ( +
+ + {step.num} + +
+

{step.title}

+

{step.description}

+
- -
+ ))}
-
-
+ + ); }; diff --git a/src/pages/NIP19Page.tsx b/src/pages/NIP19Page.tsx index 5aa4439..85c6664 100644 --- a/src/pages/NIP19Page.tsx +++ b/src/pages/NIP19Page.tsx @@ -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
Profile placeholder
; + const renderContent = () => { + switch (type) { + case 'npub': + case 'nprofile': + return ( + } + title="Profile" + description="This profile view is not yet implemented." + identifier={identifier} + /> + ); - case 'note': - // AI agent should implement note view here - return
Note placeholder
; + case 'note': + return ( + } + title="Note" + description="This note view is not yet implemented." + identifier={identifier} + /> + ); - case 'nevent': - // AI agent should implement event view here - return
Event placeholder
; + case 'nevent': + return ( + } + title="Event" + description="This event view is not yet implemented." + identifier={identifier} + /> + ); - case 'naddr': - // AI agent should implement addressable event view here - return
Addressable event placeholder
; + case 'naddr': + return ( + } + title="Addressable Event" + description="This addressable event view is not yet implemented." + identifier={identifier} + /> + ); - default: - return ; - } -} \ No newline at end of file + default: + return ; + } + }; + + return ( + +
+ {renderContent()} +
+
+ ); +} + +function PlaceholderCard({ + icon, + title, + description, + identifier, +}: { + icon: React.ReactNode; + title: string; + description: string; + identifier: string; +}) { + return ( + + +
+ {icon} +
+

{title}

+

{description}

+
+

{identifier}

+
+
+
+ ); +} diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx index 85e429d..78b3141 100644 --- a/src/pages/NotFound.tsx +++ b/src/pages/NotFound.tsx @@ -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 ( -
-
-

404

-

Oops! Page not found

- - Return to Home - + +
+

404

+

Page not found

+

+ The page you're looking for doesn't exist or has been moved. +

+
-
+ ); }; diff --git a/src/pages/Privacy.tsx b/src/pages/Privacy.tsx index 39240c6..903ff97 100644 --- a/src/pages/Privacy.tsx +++ b/src/pages/Privacy.tsx @@ -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: ( +

+ 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. +

+ ), + }, + { + title: '1. Information We Collect', + content: ( + <> +

Nostr Events

+

As a Nostr relay, we receive and store events published to our service. These events are public by design and include:

+
    +
  • Event content and metadata
  • +
  • Public keys (npub) of event authors
  • +
  • Timestamps and event signatures
  • +
  • Tags and references to other events or users
  • +
+ +

Technical Information

+

When you connect to our relay, we may collect:

+
    +
  • IP addresses for security and rate limiting purposes
  • +
  • Connection timestamps
  • +
  • WebSocket connection metadata
  • +
  • Request patterns and usage statistics
  • +
+ +

Website Analytics

+

Our website may use analytics tools to understand how users interact with our service. This may include:

+
    +
  • Page views and navigation patterns
  • +
  • Browser type and device information
  • +
  • Referrer information
  • +
+ + ), + }, + { + title: '2. How We Use Information', + content: ( + <> +

We use collected information to:

+
    +
  • Provide and maintain the relay service
  • +
  • Improve service performance and reliability
  • +
  • Prevent abuse, spam, and malicious activity
  • +
  • Comply with legal obligations
  • +
  • Analyze usage patterns to improve the service
  • +
+ + ), + }, + { + title: '3. Data Storage and Security', + content: ( + <> +

+ Public Data: 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. +

+

+ Security Measures: We implement reasonable security measures to protect our infrastructure, including: +

+
    +
  • Encrypted connections (WSS/TLS)
  • +
  • Rate limiting and abuse prevention
  • +
  • Regular security audits
  • +
  • Secure server configuration
  • +
+

+ However, no method of transmission over the internet is 100% secure, and we cannot guarantee absolute security. +

+ + ), + }, + { + title: '4. Data Retention', + content: ( + <> +

We retain Nostr events according to our relay policies, which may vary based on:

+
    +
  • Event kind and type
  • +
  • Storage capacity and resource constraints
  • +
  • Content moderation decisions
  • +
  • Legal requirements
  • +
+

+ Technical logs (IP addresses, connection data) are typically retained for 30-90 days for security and operational purposes. +

+ + ), + }, + { + title: '5. Third-Party Services', + content: ( + <> +

Our service may interact with third-party services, including:

+
    +
  • Other Nostr relays in the network
  • +
  • Content delivery networks (CDNs)
  • +
  • Analytics providers
  • +
  • Infrastructure providers
  • +
+

These third parties have their own privacy policies, and we encourage you to review them.

+ + ), + }, + { + title: '6. Your Rights and Choices', + content: ( + <> +

+ Content Control: 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. +

+

+ Pseudonymity: 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. +

+

+ Data Access: All events published to our relay are publicly queryable through standard Nostr protocols. +

+ + ), + }, + { + title: '7. Cookies and Tracking', + content: ( + <> +

Our website uses local storage and may use cookies to:

+
    +
  • Store user preferences (theme, settings)
  • +
  • Maintain login sessions
  • +
  • Remember relay configurations
  • +
+

You can control cookies through your browser settings. Disabling cookies may affect some functionality.

+ + ), + }, + { + title: '8. Children\'s Privacy', + content: ( +

+ 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. +

+ ), + }, + { + title: '9. International Users', + content: ( +

+ 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. +

+ ), + }, + { + title: '10. Changes to This Privacy Policy', + content: ( +

+ 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. +

+ ), + }, + { + title: '11. Contact Us', + content: ( +

+ 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. +

+ ), + }, + ]; + return ( -
- {/* Header */} -
-
- - - LAYER.systems - -
-
- - {/* Main Content */} -
-
- {/* Hero Section */} -
-
- -
-

Privacy Policy

-

- Last updated: December 27, 2025 -

-
- - {/* Introduction */} - - - Introduction - - -

- 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. -

-
-
- - {/* Information We Collect */} - - - 1. Information We Collect - - -

Nostr Events

-

- As a Nostr relay, we receive and store events published to our service. These events are public by design and include: -

-
    -
  • Event content and metadata
  • -
  • Public keys (npub) of event authors
  • -
  • Timestamps and event signatures
  • -
  • Tags and references to other events or users
  • -
- -

Technical Information

-

- When you connect to our relay, we may collect: -

-
    -
  • IP addresses for security and rate limiting purposes
  • -
  • Connection timestamps
  • -
  • WebSocket connection metadata
  • -
  • Request patterns and usage statistics
  • -
- -

Website Analytics

-

- Our website may use analytics tools to understand how users interact with our service. This may include: -

-
    -
  • Page views and navigation patterns
  • -
  • Browser type and device information
  • -
  • Referrer information
  • -
-
-
- - {/* How We Use Information */} - - - 2. How We Use Information - - -

- We use collected information to: -

-
    -
  • Provide and maintain the relay service
  • -
  • Improve service performance and reliability
  • -
  • Prevent abuse, spam, and malicious activity
  • -
  • Comply with legal obligations
  • -
  • Analyze usage patterns to improve the service
  • -
-
-
- - {/* Data Storage and Security */} - - - 3. Data Storage and Security - - -

- Public Data: 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. -

-

- Security Measures: We implement reasonable security measures to protect our infrastructure, including: -

-
    -
  • Encrypted connections (WSS/TLS)
  • -
  • Rate limiting and abuse prevention
  • -
  • Regular security audits
  • -
  • Secure server configuration
  • -
-

- However, no method of transmission over the internet is 100% secure, and we cannot guarantee absolute security. -

-
-
- - {/* Data Retention */} - - - 4. Data Retention - - -

- We retain Nostr events according to our relay policies, which may vary based on: -

-
    -
  • Event kind and type
  • -
  • Storage capacity and resource constraints
  • -
  • Content moderation decisions
  • -
  • Legal requirements
  • -
-

- Technical logs (IP addresses, connection data) are typically retained for 30-90 days for security and operational purposes. -

-
-
- - {/* Third-Party Services */} - - - 5. Third-Party Services - - -

- Our service may interact with third-party services, including: -

-
    -
  • Other Nostr relays in the network
  • -
  • Content delivery networks (CDNs)
  • -
  • Analytics providers
  • -
  • Infrastructure providers
  • -
-

- These third parties have their own privacy policies, and we encourage you to review them. -

-
-
- - {/* Your Rights */} - - - 6. Your Rights and Choices - - -

- Content Control: 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. -

-

- Pseudonymity: 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. -

-

- Data Access: All events published to our relay are publicly queryable through standard Nostr protocols. -

-
-
- - {/* Cookies and Tracking */} - - - 7. Cookies and Tracking - - -

- Our website uses local storage and may use cookies to: -

-
    -
  • Store user preferences (theme, settings)
  • -
  • Maintain login sessions
  • -
  • Remember relay configurations
  • -
-

- You can control cookies through your browser settings. Disabling cookies may affect some functionality. -

-
-
- - {/* Children's Privacy */} - - - 8. Children's Privacy - - -

- 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. -

-
-
- - {/* International Users */} - - - 9. International Users - - -

- 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. -

-
-
- - {/* Changes to Privacy Policy */} - - - 10. Changes to This Privacy Policy - - -

- 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. -

-
-
- - {/* Contact */} - - - 11. Contact Us - - -

- 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. -

-
-
+ +
+
+

Privacy Policy

+

Last updated: December 27, 2025

- {/* Footer Navigation */} -
-
- - Home - - - Terms of Service - -
+
+ {sections.map((section, i) => ( + + + {section.title} + + + {section.content} + + + ))}
-
-
+
+ ); } diff --git a/src/pages/Terms.tsx b/src/pages/Terms.tsx index a52efa0..f3d21cb 100644 --- a/src/pages/Terms.tsx +++ b/src/pages/Terms.tsx @@ -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: ( +

+ 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. +

+ ), + }, + { + title: '1. Service Description', + content: ( + <> +

+ 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. +

+
    +
  • The relay may be temporarily unavailable due to maintenance or technical issues
  • +
  • Events may be deleted or modified at our discretion
  • +
  • We reserve the right to refuse service to any user
  • +
+ + ), + }, + { + title: '2. User Conduct', + content: ( + <> +

By using LAYER.systems, you agree not to:

+
    +
  • Publish illegal content or content that violates applicable laws
  • +
  • Engage in spam, phishing, or other abusive behaviors
  • +
  • Attempt to disrupt or compromise the security of the relay
  • +
  • Use the service to harass, threaten, or harm others
  • +
  • Impersonate others or misrepresent your identity
  • +
  • Violate intellectual property rights
  • +
+

We reserve the right to remove content and ban users who violate these terms.

+ + ), + }, + { + title: '3. Content Policy', + content: ( + <> +

+ 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. +

+

We may remove content that:

+
    +
  • Violates applicable laws or regulations
  • +
  • Infringes on intellectual property rights
  • +
  • Contains malware or malicious code
  • +
  • Violates our content policies
  • +
+ + ), + }, + { + title: '4. Limitation of Liability', + content: ( + <> +

+ 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. +

+

We are not liable for:

+
    +
  • Any data loss or corruption
  • +
  • Service interruptions or downtime
  • +
  • Content published by users
  • +
  • Indirect, incidental, or consequential damages
  • +
  • Loss of profits or revenue
  • +
+ + ), + }, + { + title: '5. Privacy', + content: ( +

+ Your use of LAYER.systems is also governed by our{' '} + + Privacy Policy + + . Please review it to understand how we collect and use information. +

+ ), + }, + { + title: '6. Changes to Terms', + content: ( +

+ 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. +

+ ), + }, + { + title: '7. Termination', + content: ( +

+ 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. +

+ ), + }, + { + title: '8. Governing Law', + content: ( +

+ These Terms of Service are governed by and construed in accordance with applicable laws. Any disputes shall be resolved in the appropriate courts. +

+ ), + }, + { + title: '9. Contact', + content: ( +

+ If you have questions about these Terms of Service, please contact us through the Nostr protocol or via our website. +

+ ), + }, + ]; + return ( -
- {/* Header */} -
-
- - - LAYER.systems - -
-
- - {/* Main Content */} -
-
- {/* Hero Section */} -
-
- -
-

Terms of Service

-

- Last updated: December 27, 2025 -

-
- - {/* Introduction */} - - - Introduction - - -

- 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. -

-
-
- - {/* Service Description */} - - - 1. Service Description - - -

- 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. -

-
    -
  • The relay may be temporarily unavailable due to maintenance or technical issues
  • -
  • Events may be deleted or modified at our discretion
  • -
  • We reserve the right to refuse service to any user
  • -
-
-
- - {/* User Conduct */} - - - 2. User Conduct - - -

- By using LAYER.systems, you agree not to: -

-
    -
  • Publish illegal content or content that violates applicable laws
  • -
  • Engage in spam, phishing, or other abusive behaviors
  • -
  • Attempt to disrupt or compromise the security of the relay
  • -
  • Use the service to harass, threaten, or harm others
  • -
  • Impersonate others or misrepresent your identity
  • -
  • Violate intellectual property rights
  • -
-

- We reserve the right to remove content and ban users who violate these terms. -

-
-
- - {/* Content Policy */} - - - 3. Content Policy - - -

- 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. -

-

- We may remove content that: -

-
    -
  • Violates applicable laws or regulations
  • -
  • Infringes on intellectual property rights
  • -
  • Contains malware or malicious code
  • -
  • Violates our content policies
  • -
-
-
- - {/* Limitation of Liability */} - - - 4. Limitation of Liability - - -

- 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. -

-

- We are not liable for: -

-
    -
  • Any data loss or corruption
  • -
  • Service interruptions or downtime
  • -
  • Content published by users
  • -
  • Indirect, incidental, or consequential damages
  • -
  • Loss of profits or revenue
  • -
-
-
- - {/* Privacy */} - - - 5. Privacy - - -

- Your use of LAYER.systems is also governed by our{' '} - - Privacy Policy - - . Please review it to understand how we collect and use information. -

-
-
- - {/* Changes to Terms */} - - - 6. Changes to Terms - - -

- 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. -

-
-
- - {/* Termination */} - - - 7. Termination - - -

- 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. -

-
-
- - {/* Governing Law */} - - - 8. Governing Law - - -

- These Terms of Service are governed by and construed in accordance with applicable laws. Any disputes shall be resolved in the appropriate courts. -

-
-
- - {/* Contact */} - - - 9. Contact - - -

- If you have questions about these Terms of Service, please contact us through the Nostr protocol or via our website. -

-
-
+ +
+
+

Terms of Service

+

Last updated: December 27, 2025

- {/* Footer Navigation */} -
-
- - Home - - - Privacy Policy - -
+
+ {sections.map((section, i) => ( + + + {section.title} + + + {section.content} + + + ))}
-
-
+
+ ); } diff --git a/tailwind.config.ts b/tailwind.config.ts index 6d0c7ef..3145974 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -19,6 +19,10 @@ export default { } }, extend: { + fontFamily: { + sans: ['Outfit Variable', 'Outfit', 'system-ui', 'sans-serif'], + serif: ['Bitter Variable', 'Bitter', 'Georgia', 'serif'], + }, colors: { border: 'hsl(var(--border))', input: 'hsl(var(--input))',