mirror of
https://github.com/layer-systems/website.git
synced 2026-09-13 14:14:07 +02:00
Refresh dashboard styling with bold retro cards
This commit is contained in:
325
DESIGN.md
Normal file
325
DESIGN.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# LAYER.systems Design Direction
|
||||
|
||||
## 1. Design Intent
|
||||
|
||||
LAYER.systems should feel like Nostr relay infrastructure made visible: direct, fast, public, and protocol-native. The visual direction takes cues from bold editorial community sites such as `einundzwanzig.space`: strong navigation, large type, ticker energy, clear content bands, and a public-broadcast feeling.
|
||||
|
||||
This is not a clone. LAYER.systems needs its own identity: orange, infrastructural, decentralized, and practical. The site should make the relay endpoint the center of the experience, not a footnote.
|
||||
|
||||
## 2. Reference Translation
|
||||
|
||||
Inspired by `einundzwanzig.space`:
|
||||
|
||||
- Strong top navigation with grouped links.
|
||||
- Large typographic hero with direct messaging.
|
||||
- Horizontal ticker / marquee energy for relay URLs, protocol phrases, and network language.
|
||||
- Clear content bands for relay status, usage, and connection steps.
|
||||
- Editorial confidence instead of generic landing-page polish.
|
||||
- Playful tone without losing operational credibility.
|
||||
|
||||
Translate for LAYER.systems:
|
||||
|
||||
- Use an orange-led palette instead of purple.
|
||||
- Replace podcast/news hierarchy with relay endpoint and Nostr infrastructure hierarchy.
|
||||
- Replace sponsor/content blocks with relay, protocol, client, and network modules.
|
||||
- Use `wss://relay.layer.systems` as a visible recurring design motif.
|
||||
- Make the copyable relay URL feel like the main product surface.
|
||||
|
||||
## 3. Personality
|
||||
|
||||
The interface should be:
|
||||
|
||||
- Public, not gated.
|
||||
- Bold, not glossy.
|
||||
- Operational, not corporate.
|
||||
- Protocol-aware, not protocol-obscure.
|
||||
- Human in tone, calm in structure.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Generic orange gradient SaaS aesthetics.
|
||||
- Overly soft pastel community branding.
|
||||
- Crypto-bro visual language.
|
||||
- Dense technical jargon in primary UI copy.
|
||||
- Stock-photo hero sections.
|
||||
|
||||
## 4. Visual System
|
||||
|
||||
### Color Palette
|
||||
|
||||
Use orange as the identity anchor, but avoid a one-note all-orange interface.
|
||||
|
||||
Suggested palette:
|
||||
|
||||
- `Signal Orange`: `#F97316` for primary actions and identity marks.
|
||||
- `Deep Ink`: `#18110D` for dark text, footer, and hard borders.
|
||||
- `Relay Amber`: `#FACC15` for secondary highlights and ticker details.
|
||||
- `Paper`: `#FFF7E8` for warm page background.
|
||||
- `Chalk`: `#FFFFFF` for content surfaces.
|
||||
- `Clay`: `#7C3F1D` for subdued labels and supporting text.
|
||||
- `Protocol Green`: `#17A673` for live/verified states.
|
||||
- `Network Blue`: `#2563EB` as a sparing cool accent.
|
||||
|
||||
Usage rules:
|
||||
|
||||
- Orange should lead, not flood.
|
||||
- Use ink-heavy contrast and crisp borders for readability.
|
||||
- Use amber, green, and blue sparingly to prevent the interface from becoming monochrome.
|
||||
- Relay cards should remain legible before they are decorative.
|
||||
|
||||
### Typography
|
||||
|
||||
The type system should feel editorial and independent.
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- Display: bold grotesk weight for hero and section titles.
|
||||
- Body: readable sans with warmth and strong numerals.
|
||||
- Mono: restrained monospace for relay URLs, event kinds, public keys, and tags.
|
||||
|
||||
Implementation preference:
|
||||
|
||||
- Use `@fontsource` packages when available.
|
||||
- Use CSS variables for font families.
|
||||
- Keep body text comfortable on content-heavy sections.
|
||||
- Keep hero type large and compact, but never allow it to overflow mobile widths.
|
||||
|
||||
## 5. Layout
|
||||
|
||||
### Page Structure
|
||||
|
||||
The first version should be a single-page relay experience:
|
||||
|
||||
1. Sticky navigation
|
||||
2. Relay ticker
|
||||
3. Hero / identity
|
||||
4. Copyable relay endpoint
|
||||
5. Relay feature modules
|
||||
6. Client connection steps
|
||||
7. Footer with terms, privacy, and dashboard links
|
||||
|
||||
### Navigation
|
||||
|
||||
The nav should feel practical and slightly editorial:
|
||||
|
||||
- Left: LAYER.systems wordmark.
|
||||
- Center: Relay, Signal, Connect, Explore.
|
||||
- Right: `LoginArea`.
|
||||
- Mobile: compact header with persistent login/account access.
|
||||
|
||||
Use strong text links and clear hover/focus treatment. Avoid oversized pill buttons for every nav item.
|
||||
|
||||
### Ticker
|
||||
|
||||
Add a horizontal ticker near the top of the page.
|
||||
|
||||
Potential ticker phrases:
|
||||
|
||||
- `LAYER.systems`
|
||||
- `public Nostr relay`
|
||||
- `wss://relay.layer.systems`
|
||||
- `open protocols`
|
||||
- `relay-first social`
|
||||
- `NIP-aware infrastructure`
|
||||
- `bring your own client`
|
||||
- `events over platforms`
|
||||
|
||||
Ticker behavior:
|
||||
|
||||
- Continuous motion on desktop.
|
||||
- Static fallback with `prefers-reduced-motion`.
|
||||
- High contrast.
|
||||
- No essential information should exist only in the ticker.
|
||||
|
||||
### Hero
|
||||
|
||||
The hero should be typographic and signal-heavy.
|
||||
|
||||
Content:
|
||||
|
||||
- Main title: `Relay signal for the open social web.`
|
||||
- Supporting line: a concise statement about public Nostr relay infrastructure.
|
||||
- Primary CTA: jump to the relay URL.
|
||||
- Secondary CTA: jump to connection steps.
|
||||
|
||||
Visual treatment:
|
||||
|
||||
- Large editorial headline.
|
||||
- Copyable endpoint module.
|
||||
- Subtle grid or protocol texture is acceptable if it does not reduce readability.
|
||||
- Use real relay/data motifs rather than abstract decorative blobs.
|
||||
|
||||
## 6. Components
|
||||
|
||||
### Relay Endpoint Card
|
||||
|
||||
Required elements:
|
||||
|
||||
- Relay URL.
|
||||
- Copy button with copied state.
|
||||
- Access mode.
|
||||
- Network/protocol labels.
|
||||
- Clear read/write language when applicable.
|
||||
|
||||
Design:
|
||||
|
||||
- Rectangular panel with `8px` or less radius.
|
||||
- Crisp border and offset shadow.
|
||||
- Relay URL must wrap safely on small screens.
|
||||
- Copy action must remain reachable on mobile.
|
||||
|
||||
### Feature Cards
|
||||
|
||||
Required elements:
|
||||
|
||||
- Icon.
|
||||
- Short title.
|
||||
- One practical sentence.
|
||||
|
||||
Design:
|
||||
|
||||
- Rectangular cards.
|
||||
- Crisp border.
|
||||
- Subtle hover shift.
|
||||
- No nested cards.
|
||||
|
||||
### Connection Steps
|
||||
|
||||
Required elements:
|
||||
|
||||
- Step number.
|
||||
- Client action.
|
||||
- Relay URL in the add-relay step.
|
||||
|
||||
Design:
|
||||
|
||||
- Structured rows or blocks.
|
||||
- Step numbers should be visually strong.
|
||||
- Code text should wrap cleanly.
|
||||
|
||||
### Empty States
|
||||
|
||||
If live relay status or event lists are added later, empty states should be minimalist and practical:
|
||||
|
||||
- Explain what data will appear.
|
||||
- Include a short hint about adding `wss://relay.layer.systems` from any Nostr client.
|
||||
- Avoid speculative promises about uptime or persistence.
|
||||
|
||||
## 7. Motion
|
||||
|
||||
Motion should feel like signal movement, not decoration.
|
||||
|
||||
Use:
|
||||
|
||||
- Ticker movement.
|
||||
- Subtle hover shifts on cards and CTAs.
|
||||
- Focus-visible rings with a quick transition.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Heavy parallax.
|
||||
- Decorative floating orbs.
|
||||
- Motion that distracts from reading the relay URL.
|
||||
|
||||
Respect `prefers-reduced-motion`.
|
||||
|
||||
## 8. Imagery and Texture
|
||||
|
||||
This site does not need stock photography.
|
||||
|
||||
Preferred visual motifs:
|
||||
|
||||
- Relay/network linework.
|
||||
- Terminal-inspired grid texture.
|
||||
- Endpoint/code fragments.
|
||||
- Protocol tags and event-kind labels.
|
||||
- Simple infrastructure diagrams.
|
||||
|
||||
Texture direction:
|
||||
|
||||
- Light paper/noise texture is acceptable.
|
||||
- Grid backgrounds are acceptable.
|
||||
- Keep texture subtle enough that body text remains AA compliant.
|
||||
|
||||
## 9. Accessibility
|
||||
|
||||
Requirements:
|
||||
|
||||
- WCAG 2.1 AA contrast for text and controls.
|
||||
- Keyboard-accessible navigation and controls.
|
||||
- Visible focus states.
|
||||
- Semantic headings.
|
||||
- Descriptive alt text for static images.
|
||||
- Empty `alt` for purely decorative images.
|
||||
- No unsafe HTML injection.
|
||||
|
||||
## 10. Responsive Behavior
|
||||
|
||||
Mobile:
|
||||
|
||||
- Single-column flow.
|
||||
- Sticky compact nav.
|
||||
- Ticker can become static.
|
||||
- Relay URL must wrap without layout overflow.
|
||||
- Hero type must scale by breakpoint, not viewport-width formulas.
|
||||
|
||||
Tablet:
|
||||
|
||||
- Two-column section layouts where useful.
|
||||
- Endpoint card remains prominent.
|
||||
|
||||
Desktop:
|
||||
|
||||
- Editorial grid with asymmetric section rhythm.
|
||||
- Relay endpoint and hero copy share the first viewport.
|
||||
- Keep primary content width controlled for readability.
|
||||
|
||||
## 11. Copy Tone
|
||||
|
||||
Tone should be direct and protocol-native.
|
||||
|
||||
Good:
|
||||
|
||||
- `Relay signal for the open social web.`
|
||||
- `A public Nostr relay for portable social identity.`
|
||||
- `Add the relay URL from any Nostr client.`
|
||||
- `Events over platforms.`
|
||||
|
||||
Avoid:
|
||||
|
||||
- `Revolutionizing social engagement`
|
||||
- `The ultimate decentralized ecosystem`
|
||||
- `Next-generation community platform`
|
||||
- `Seamless innovative experiences`
|
||||
|
||||
## 12. Implementation Notes
|
||||
|
||||
- Use existing shadcn/ui primitives.
|
||||
- Keep cards at `8px` radius or less unless a local component requires otherwise.
|
||||
- Use `cn()` for class composition when class branching is needed.
|
||||
- Use lucide icons where icons are needed.
|
||||
- Define color and font tokens in CSS variables.
|
||||
- Keep sections as full-width bands with constrained inner content.
|
||||
- Do not nest UI cards inside UI cards.
|
||||
- Do not add unsafe HTML rendering for Nostr content.
|
||||
- Sanitize all event-sourced URLs before rendering.
|
||||
|
||||
## 13. Documentation Tie-In
|
||||
|
||||
The `/docs` folder can document the design system once implementation expands.
|
||||
|
||||
Recommended docs:
|
||||
|
||||
- `/docs/design-system.md`: colors, typography, spacing, components.
|
||||
- `/docs/content-guidelines.md`: tone, protocol language, relay usage guidance.
|
||||
- `/docs/nostr-data.md`: relay-facing event kinds, tags, validation, and query patterns.
|
||||
|
||||
## 14. Design Acceptance Criteria
|
||||
|
||||
- The site clearly reads as LAYER.systems in the first viewport.
|
||||
- The visual direction feels inspired by `einundzwanzig.space` but has its own orange Nostr relay identity.
|
||||
- The relay endpoint is visually central, not secondary.
|
||||
- The interface remains readable and usable at 360px width.
|
||||
- Motion respects `prefers-reduced-motion`.
|
||||
- UI contrast meets WCAG 2.1 AA.
|
||||
- The design avoids generic orange SaaS styling.
|
||||
@@ -14,7 +14,7 @@ export function DashboardStats({ pubkey }: DashboardStatsProps) {
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Card key={i}>
|
||||
<Card key={i} className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-4 w-4 rounded" />
|
||||
@@ -41,39 +41,39 @@ export function DashboardStats({ pubkey }: DashboardStatsProps) {
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Loaded Events</CardTitle>
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
<CardTitle className="text-sm font-black uppercase">Loaded Events</CardTitle>
|
||||
<FileText className="h-4 w-4 text-primary" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.totalEvents}</div>
|
||||
<div className="font-mono text-3xl font-black">{stats.totalEvents}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
All events loaded
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Event Types</CardTitle>
|
||||
<Hash className="h-4 w-4 text-muted-foreground" />
|
||||
<CardTitle className="text-sm font-black uppercase">Event Types</CardTitle>
|
||||
<Hash className="h-4 w-4 text-primary" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{uniqueKinds}</div>
|
||||
<div className="font-mono text-3xl font-black">{uniqueKinds}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Different kinds used
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Most Used</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
<CardTitle className="text-sm font-black uppercase">Most Used</CardTitle>
|
||||
<Activity className="h-4 w-4 text-primary" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
<div className="font-mono text-3xl font-black">
|
||||
{Object.entries(stats.eventsByKind).length > 0
|
||||
? `Kind ${
|
||||
Object.entries(stats.eventsByKind).sort(
|
||||
@@ -94,13 +94,13 @@ export function DashboardStats({ pubkey }: DashboardStatsProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Last Activity</CardTitle>
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<CardTitle className="text-sm font-black uppercase">Last Activity</CardTitle>
|
||||
<Clock className="h-4 w-4 text-primary" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
<div className="font-mono text-3xl font-black">
|
||||
{stats.lastActivity
|
||||
? new Date(stats.lastActivity * 1000).toLocaleDateString(
|
||||
undefined,
|
||||
|
||||
@@ -122,7 +122,7 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-40 mb-2" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
@@ -146,9 +146,9 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
|
||||
|
||||
if (isError || !stats) {
|
||||
return (
|
||||
<Card className="border-dashed">
|
||||
<Card className="rounded-none border-2 border-dashed border-foreground bg-card shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Your Events</CardTitle>
|
||||
<CardTitle className="font-black uppercase">Your Events</CardTitle>
|
||||
<CardDescription>
|
||||
Explore your published events and manage deletion requests.
|
||||
</CardDescription>
|
||||
@@ -164,9 +164,9 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
|
||||
|
||||
if (stats.events.length === 0) {
|
||||
return (
|
||||
<Card className="border-dashed">
|
||||
<Card className="rounded-none border-2 border-dashed border-foreground bg-card shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Your Events</CardTitle>
|
||||
<CardTitle className="font-black uppercase">Your Events</CardTitle>
|
||||
<CardDescription>
|
||||
Explore your published events and manage deletion requests.
|
||||
</CardDescription>
|
||||
@@ -185,10 +185,10 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="overflow-hidden">
|
||||
<Card className="overflow-hidden rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<CardTitle className="break-words">Your Events</CardTitle>
|
||||
<CardTitle className="break-words font-black uppercase">Your Events</CardTitle>
|
||||
<CardDescription className="break-words">
|
||||
Browse your published events and request deletion directly from your dashboard.
|
||||
</CardDescription>
|
||||
@@ -197,7 +197,7 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
className="pl-8"
|
||||
className="rounded-none border-2 border-foreground pl-8"
|
||||
placeholder="Search content..."
|
||||
value={search}
|
||||
onChange={(event) => {
|
||||
@@ -221,11 +221,11 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className="flex flex-col gap-2 rounded-lg border bg-card/40 p-3 transition hover:bg-accent/40 sm:flex-row sm:items-center sm:justify-between"
|
||||
className="flex flex-col gap-2 border-2 border-foreground bg-card p-3 transition hover:-translate-y-0.5 hover:bg-secondary sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div className="space-y-1 min-w-0 overflow-hidden">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
<Badge variant="secondary" className="shrink-0 rounded-none border border-foreground text-xs font-black uppercase">
|
||||
{label}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
@@ -252,7 +252,7 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="whitespace-nowrap"
|
||||
className="whitespace-nowrap rounded-none border-2 border-foreground font-bold"
|
||||
onClick={() => handleOpenDialog(event)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
@@ -268,9 +268,9 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="space-y-3 py-2 text-sm">
|
||||
<div className="rounded-md bg-muted px-3 py-2">
|
||||
<div className="border-2 border-foreground bg-muted px-3 py-2">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Badge variant="secondary" className="text-2xs">
|
||||
<Badge variant="secondary" className="rounded-none border border-foreground text-2xs">
|
||||
{label}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
@@ -288,7 +288,7 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
|
||||
<Textarea
|
||||
value={reason}
|
||||
onChange={(event) => setReason(event.target.value)}
|
||||
className="min-h-[80px] text-sm"
|
||||
className="min-h-[80px] rounded-none border-2 border-foreground text-sm"
|
||||
placeholder="Explain why you are requesting this deletion."
|
||||
/>
|
||||
</div>
|
||||
@@ -337,6 +337,7 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-none border-2 border-foreground font-bold"
|
||||
disabled={pageSafe <= 1}
|
||||
onClick={() => setPage((current) => Math.max(current - 1, 1))}
|
||||
>
|
||||
@@ -354,6 +355,7 @@ export function EventExplorer({ pubkey }: EventExplorerProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-none border-2 border-foreground font-bold"
|
||||
disabled={pageSafe >= totalPages}
|
||||
onClick={() =>
|
||||
setPage((current) => Math.min(current + 1, totalPages))
|
||||
|
||||
@@ -109,7 +109,7 @@ export function EventKindsChart() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<Card className="flex flex-col rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<Skeleton className="h-4 w-48 mt-2" />
|
||||
@@ -129,7 +129,7 @@ export function EventKindsChart() {
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<Card className="flex flex-col rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Event Kinds Distribution</CardTitle>
|
||||
<CardDescription>Log in to see your event distribution</CardDescription>
|
||||
@@ -143,7 +143,7 @@ export function EventKindsChart() {
|
||||
|
||||
if (!stats || chartData.length === 0) {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<Card className="flex flex-col rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Event Kinds Distribution</CardTitle>
|
||||
<CardDescription>Your published event types</CardDescription>
|
||||
@@ -156,9 +156,9 @@ export function EventKindsChart() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col overflow-hidden">
|
||||
<Card className="flex flex-col overflow-hidden rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Event Kinds Distribution</CardTitle>
|
||||
<CardTitle className="font-black uppercase">Event Kinds Distribution</CardTitle>
|
||||
<CardDescription>Your top 5 event types</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
|
||||
@@ -88,7 +88,7 @@ export function RecentActivityChart() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-40 mb-2" />
|
||||
<Skeleton className="h-4 w-60" />
|
||||
@@ -102,7 +102,7 @@ export function RecentActivityChart() {
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<CardDescription>Last 30 days</CardDescription>
|
||||
@@ -118,7 +118,7 @@ export function RecentActivityChart() {
|
||||
|
||||
if (chartData.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<CardDescription>Last 30 days</CardDescription>
|
||||
@@ -133,9 +133,9 @@ export function RecentActivityChart() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<Card className="overflow-hidden rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<CardTitle className="font-black uppercase">Recent Activity</CardTitle>
|
||||
<CardDescription>
|
||||
Your event activity over the last 30 days
|
||||
</CardDescription>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function RecentActivityList({ pubkey }: RecentActivityListProps) {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-32 mb-2" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
@@ -51,9 +51,9 @@ export function RecentActivityList({ pubkey }: RecentActivityListProps) {
|
||||
|
||||
if (!stats || stats.recentEvents.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<CardTitle className="font-black uppercase">Recent Activity</CardTitle>
|
||||
<CardDescription>Your latest events</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -66,9 +66,9 @@ export function RecentActivityList({ pubkey }: RecentActivityListProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<CardTitle className="font-black uppercase">Recent Activity</CardTitle>
|
||||
<CardDescription>Your latest {stats.recentEvents.length} events</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -91,7 +91,7 @@ export function RecentActivityList({ pubkey }: RecentActivityListProps) {
|
||||
>
|
||||
<div className="space-y-1 flex-1 min-w-0 overflow-hidden">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
<Badge variant="secondary" className="shrink-0 rounded-none border border-foreground text-xs font-black uppercase">
|
||||
{kindName}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Home, LayoutDashboard, FileText, Download } from 'lucide-react';
|
||||
import { Download, FileText, Home, LayoutDashboard, Radio } from 'lucide-react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -40,17 +40,28 @@ export function AppSidebar() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
<Sidebar className="border-r-2 border-foreground">
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
||||
<div className="m-2 mb-4 border-2 border-foreground bg-primary p-3 text-primary-foreground shadow-[4px_4px_0_hsl(var(--foreground))]">
|
||||
<div className="flex items-center gap-2 font-black uppercase leading-none">
|
||||
<Radio className="h-5 w-5" />
|
||||
LAYER.systems
|
||||
</div>
|
||||
<p className="mt-2 text-xs font-bold uppercase">Relay dashboard</p>
|
||||
</div>
|
||||
<SidebarGroupLabel className="font-black uppercase text-foreground">Navigation</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{navigationItems.map((item) => {
|
||||
const isActive = location.pathname === item.url;
|
||||
return (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild isActive={isActive}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={isActive}
|
||||
className="rounded-none font-bold data-[active=true]:border-2 data-[active=true]:border-foreground data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
|
||||
>
|
||||
<Link to={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
|
||||
156
src/index.css
156
src/index.css
@@ -1,42 +1,44 @@
|
||||
@import "@fontsource-variable/inter";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 14.9020%;
|
||||
--background: 42 100% 97%;
|
||||
--foreground: 20 23% 8%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 14.9020%;
|
||||
--card-foreground: 20 23% 8%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 14.9020%;
|
||||
--primary: 37.6923 92.1260% 50.1961%;
|
||||
--primary-foreground: 0 0% 0%;
|
||||
--secondary: 220.0000 14.2857% 95.8824%;
|
||||
--secondary-foreground: 215 13.7931% 34.1176%;
|
||||
--muted: 210 20.0000% 98.0392%;
|
||||
--muted-foreground: 220 8.9362% 46.0784%;
|
||||
--accent: 48.0000 100.0000% 96.0784%;
|
||||
--accent-foreground: 22.7273 82.5000% 31.3725%;
|
||||
--popover-foreground: 20 23% 8%;
|
||||
--primary: 28 96% 52%;
|
||||
--primary-foreground: 20 23% 8%;
|
||||
--secondary: 39 100% 90%;
|
||||
--secondary-foreground: 20 23% 8%;
|
||||
--muted: 38 42% 91%;
|
||||
--muted-foreground: 24 12% 35%;
|
||||
--accent: 49 95% 58%;
|
||||
--accent-foreground: 20 23% 8%;
|
||||
--destructive: 0 84.2365% 60.1961%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 220 13.0435% 90.9804%;
|
||||
--input: 220 13.0435% 90.9804%;
|
||||
--ring: 37.6923 92.1260% 50.1961%;
|
||||
--chart-1: 37.6923 92.1260% 50.1961%;
|
||||
--chart-2: 32.1327 94.6188% 43.7255%;
|
||||
--chart-3: 25.9649 90.4762% 37.0588%;
|
||||
--chart-4: 22.7273 82.5000% 31.3725%;
|
||||
--chart-5: 21.7143 77.7778% 26.4706%;
|
||||
--sidebar: 210 20.0000% 98.0392%;
|
||||
--sidebar-foreground: 0 0% 14.9020%;
|
||||
--sidebar-primary: 37.6923 92.1260% 50.1961%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 48.0000 100.0000% 96.0784%;
|
||||
--sidebar-accent-foreground: 22.7273 82.5000% 31.3725%;
|
||||
--sidebar-border: 220 13.0435% 90.9804%;
|
||||
--sidebar-ring: 37.6923 92.1260% 50.1961%;
|
||||
--font-sans: Inter, sans-serif;
|
||||
--border: 25 31% 82%;
|
||||
--input: 25 31% 82%;
|
||||
--ring: 28 96% 52%;
|
||||
--chart-1: 28 96% 52%;
|
||||
--chart-2: 49 95% 58%;
|
||||
--chart-3: 159 65% 36%;
|
||||
--chart-4: 210 68% 42%;
|
||||
--chart-5: 15 80% 42%;
|
||||
--sidebar: 42 100% 97%;
|
||||
--sidebar-foreground: 20 23% 8%;
|
||||
--sidebar-primary: 28 96% 52%;
|
||||
--sidebar-primary-foreground: 20 23% 8%;
|
||||
--sidebar-accent: 49 95% 58%;
|
||||
--sidebar-accent-foreground: 20 23% 8%;
|
||||
--sidebar-border: 25 31% 82%;
|
||||
--sidebar-ring: 28 96% 52%;
|
||||
--font-sans: "Inter Variable", Inter, sans-serif;
|
||||
--font-serif: Source Serif 4, serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--radius: 0.375rem;
|
||||
@@ -59,39 +61,39 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 9.0196%;
|
||||
--foreground: 0 0% 89.8039%;
|
||||
--card: 0 0% 14.9020%;
|
||||
--card-foreground: 0 0% 89.8039%;
|
||||
--popover: 0 0% 14.9020%;
|
||||
--popover-foreground: 0 0% 89.8039%;
|
||||
--primary: 37.6923 92.1260% 50.1961%;
|
||||
--primary-foreground: 0 0% 0%;
|
||||
--secondary: 0 0% 14.9020%;
|
||||
--secondary-foreground: 0 0% 89.8039%;
|
||||
--muted: 0 0% 12.1569%;
|
||||
--muted-foreground: 0 0% 63.9216%;
|
||||
--accent: 22.7273 82.5000% 31.3725%;
|
||||
--accent-foreground: 48 96.6387% 76.6667%;
|
||||
--background: 20 23% 8%;
|
||||
--foreground: 42 100% 95%;
|
||||
--card: 20 18% 12%;
|
||||
--card-foreground: 42 100% 95%;
|
||||
--popover: 20 18% 12%;
|
||||
--popover-foreground: 42 100% 95%;
|
||||
--primary: 31 100% 57%;
|
||||
--primary-foreground: 20 23% 8%;
|
||||
--secondary: 22 18% 17%;
|
||||
--secondary-foreground: 42 100% 95%;
|
||||
--muted: 23 15% 19%;
|
||||
--muted-foreground: 37 24% 72%;
|
||||
--accent: 49 95% 58%;
|
||||
--accent-foreground: 20 23% 8%;
|
||||
--destructive: 0 84.2365% 60.1961%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 0 0% 25.0980%;
|
||||
--input: 0 0% 25.0980%;
|
||||
--ring: 37.6923 92.1260% 50.1961%;
|
||||
--chart-1: 43.2558 96.4126% 56.2745%;
|
||||
--chart-2: 32.1327 94.6188% 43.7255%;
|
||||
--chart-3: 22.7273 82.5000% 31.3725%;
|
||||
--chart-4: 25.9649 90.4762% 37.0588%;
|
||||
--chart-5: 22.7273 82.5000% 31.3725%;
|
||||
--sidebar: 0 0% 5.8824%;
|
||||
--sidebar-foreground: 0 0% 89.8039%;
|
||||
--sidebar-primary: 37.6923 92.1260% 50.1961%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 22.7273 82.5000% 31.3725%;
|
||||
--sidebar-accent-foreground: 48 96.6387% 76.6667%;
|
||||
--sidebar-border: 0 0% 25.0980%;
|
||||
--sidebar-ring: 37.6923 92.1260% 50.1961%;
|
||||
--font-sans: Inter, sans-serif;
|
||||
--border: 24 18% 28%;
|
||||
--input: 24 18% 28%;
|
||||
--ring: 31 100% 57%;
|
||||
--chart-1: 31 100% 57%;
|
||||
--chart-2: 49 95% 58%;
|
||||
--chart-3: 159 65% 46%;
|
||||
--chart-4: 210 68% 58%;
|
||||
--chart-5: 15 80% 52%;
|
||||
--sidebar: 20 23% 8%;
|
||||
--sidebar-foreground: 42 100% 95%;
|
||||
--sidebar-primary: 31 100% 57%;
|
||||
--sidebar-primary-foreground: 20 23% 8%;
|
||||
--sidebar-accent: 22 18% 17%;
|
||||
--sidebar-accent-foreground: 42 100% 95%;
|
||||
--sidebar-border: 24 18% 28%;
|
||||
--sidebar-ring: 31 100% 57%;
|
||||
--font-sans: "Inter Variable", Inter, sans-serif;
|
||||
--font-serif: Source Serif 4, serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--radius: 0.375rem;
|
||||
@@ -119,5 +121,37 @@
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: var(--font-sans);
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.relay-ticker-track {
|
||||
animation: relay-marquee 30s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes relay-marquee {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
.relay-ticker-track {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { RecentActivityList } from '@/components/dashboard/RecentActivityList';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { Activity, InfoIcon } from 'lucide-react';
|
||||
import { Account, useLoggedInAccounts } from '@/hooks/useLoggedInAccounts';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
|
||||
@@ -21,17 +21,17 @@ export function Dashboard() {
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="flex min-h-screen w-full overflow-x-hidden">
|
||||
<div className="flex min-h-screen w-full overflow-x-hidden bg-background">
|
||||
<AppSidebar />
|
||||
<main className="flex-1 min-w-0">
|
||||
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background px-4 lg:h-[60px] lg:px-6">
|
||||
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b-2 border-foreground bg-background/95 px-4 backdrop-blur lg:h-[60px] lg:px-6">
|
||||
<SidebarTrigger />
|
||||
<h1 className="text-lg font-semibold md:text-xl truncate">Dashboard</h1>
|
||||
<h1 className="truncate text-lg font-black uppercase md:text-xl">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
|
||||
<div className="flex-1 space-y-6 overflow-x-hidden bg-[linear-gradient(90deg,hsl(var(--border))_1px,transparent_1px),linear-gradient(180deg,hsl(var(--border))_1px,transparent_1px)] bg-[size:32px_32px] p-4 md:p-6 lg:p-8">
|
||||
{!user ? (
|
||||
<Card className="border-dashed">
|
||||
<Card className="rounded-none border-2 border-dashed border-foreground bg-card shadow-[6px_6px_0_hsl(var(--primary))]">
|
||||
<CardContent className="py-12 px-8 text-center">
|
||||
<div className="max-w-sm mx-auto space-y-4">
|
||||
<Alert>
|
||||
@@ -45,12 +45,16 @@ export function Dashboard() {
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl md:text-3xl font-bold tracking-tight break-words">
|
||||
<div className="border-2 border-foreground bg-card p-5 shadow-[6px_6px_0_hsl(var(--primary))]">
|
||||
<div className="mb-4 inline-flex items-center gap-2 border-2 border-foreground bg-primary px-3 py-1 text-xs font-black uppercase text-primary-foreground">
|
||||
<Activity className="h-3.5 w-3.5" />
|
||||
Account signal
|
||||
</div>
|
||||
<h2 className="break-words text-3xl font-black leading-none md:text-5xl">
|
||||
Welcome back {currentUser ? getDisplayName(currentUser) : ''}!
|
||||
</h2>
|
||||
<p className="text-sm md:text-base text-muted-foreground">
|
||||
Here's an overview of your Nostr activity and statistics.
|
||||
<p className="mt-3 max-w-2xl text-sm font-medium leading-7 text-muted-foreground md:text-base">
|
||||
Your Nostr activity, event kinds, and recent relay-facing signal in one place.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,17 +11,17 @@ export function DashboardEvents() {
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="flex min-h-screen w-full overflow-x-hidden">
|
||||
<div className="flex min-h-screen w-full overflow-x-hidden bg-background">
|
||||
<AppSidebar />
|
||||
<main className="flex-1 min-w-0">
|
||||
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background px-4 lg:h-[60px] lg:px-6">
|
||||
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b-2 border-foreground bg-background/95 px-4 backdrop-blur lg:h-[60px] lg:px-6">
|
||||
<SidebarTrigger />
|
||||
<h1 className="text-lg font-semibold md:text-xl truncate">My Events</h1>
|
||||
<h1 className="truncate text-lg font-black uppercase md:text-xl">My Events</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
|
||||
<div className="flex-1 space-y-6 overflow-x-hidden bg-[linear-gradient(90deg,hsl(var(--border))_1px,transparent_1px),linear-gradient(180deg,hsl(var(--border))_1px,transparent_1px)] bg-[size:32px_32px] p-4 md:p-6 lg:p-8">
|
||||
{!user ? (
|
||||
<Card className="border-dashed">
|
||||
<Card className="rounded-none border-2 border-dashed border-foreground bg-card shadow-[6px_6px_0_hsl(var(--primary))]">
|
||||
<CardContent className="py-12 px-8 text-center">
|
||||
<div className="max-w-sm mx-auto space-y-4">
|
||||
<Alert>
|
||||
@@ -35,11 +35,14 @@ export function DashboardEvents() {
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl md:text-3xl font-bold tracking-tight break-words">
|
||||
<div className="border-2 border-foreground bg-card p-5 shadow-[6px_6px_0_hsl(var(--primary))]">
|
||||
<div className="mb-4 inline-flex border-2 border-foreground bg-primary px-3 py-1 text-xs font-black uppercase text-primary-foreground">
|
||||
Event explorer
|
||||
</div>
|
||||
<h2 className="break-words text-3xl font-black leading-none md:text-5xl">
|
||||
Your Nostr events
|
||||
</h2>
|
||||
<p className="text-sm md:text-base text-muted-foreground max-w-2xl">
|
||||
<p className="mt-3 max-w-2xl text-sm font-medium leading-7 text-muted-foreground md:text-base">
|
||||
Browse all events you have published on Nostr, search through their content,
|
||||
and publish deletion requests when you want something removed.
|
||||
</p>
|
||||
|
||||
@@ -89,17 +89,17 @@ export function DashboardExport() {
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="flex min-h-screen w-full overflow-x-hidden">
|
||||
<div className="flex min-h-screen w-full overflow-x-hidden bg-background">
|
||||
<AppSidebar />
|
||||
<main className="flex-1 min-w-0">
|
||||
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b bg-background px-4 lg:h-[60px] lg:px-6">
|
||||
<div className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b-2 border-foreground bg-background/95 px-4 backdrop-blur lg:h-[60px] lg:px-6">
|
||||
<SidebarTrigger />
|
||||
<h1 className="text-lg font-semibold md:text-xl truncate">Export Following List</h1>
|
||||
<h1 className="truncate text-lg font-black uppercase md:text-xl">Export Following List</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6 p-4 md:p-6 lg:p-8 overflow-x-hidden">
|
||||
<div className="flex-1 space-y-6 overflow-x-hidden bg-[linear-gradient(90deg,hsl(var(--border))_1px,transparent_1px),linear-gradient(180deg,hsl(var(--border))_1px,transparent_1px)] bg-[size:32px_32px] p-4 md:p-6 lg:p-8">
|
||||
{!user ? (
|
||||
<Card className="border-dashed">
|
||||
<Card className="rounded-none border-2 border-dashed border-foreground bg-card shadow-[6px_6px_0_hsl(var(--primary))]">
|
||||
<CardContent className="py-12 px-8 text-center">
|
||||
<div className="max-w-sm mx-auto space-y-4">
|
||||
<Alert>
|
||||
@@ -112,10 +112,13 @@ export function DashboardExport() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
<Card>
|
||||
<div className="mx-auto max-w-3xl space-y-6">
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-[6px_6px_0_hsl(var(--primary))]">
|
||||
<CardHeader>
|
||||
<CardTitle>Backup Your Following List</CardTitle>
|
||||
<div className="mb-2 inline-flex w-fit border-2 border-foreground bg-primary px-3 py-1 text-xs font-black uppercase text-primary-foreground">
|
||||
Kind 3 backup
|
||||
</div>
|
||||
<CardTitle className="text-3xl font-black leading-none">Backup your following list</CardTitle>
|
||||
<CardDescription>
|
||||
Export your contact list as a JSON file for backup or migration purposes.
|
||||
</CardDescription>
|
||||
@@ -148,11 +151,11 @@ export function DashboardExport() {
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground shadow-none">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Users className="h-6 w-6 text-primary" />
|
||||
<div className="flex h-12 w-12 items-center justify-center border-2 border-foreground bg-primary text-primary-foreground">
|
||||
<Users className="h-6 w-6 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Following</p>
|
||||
@@ -162,11 +165,11 @@ export function DashboardExport() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground shadow-none">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Calendar className="h-6 w-6 text-primary" />
|
||||
<div className="flex h-12 w-12 items-center justify-center border-2 border-foreground bg-primary text-primary-foreground">
|
||||
<Calendar className="h-6 w-6 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Last Updated</p>
|
||||
@@ -194,8 +197,8 @@ export function DashboardExport() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border p-4 space-y-2">
|
||||
<h3 className="font-semibold text-sm">What's included:</h3>
|
||||
<div className="space-y-2 border-2 border-foreground bg-secondary p-4">
|
||||
<h3 className="text-sm font-black uppercase">What's included:</h3>
|
||||
<ul className="text-sm text-muted-foreground space-y-1">
|
||||
<li>• Complete list of all {followingCount} accounts you follow</li>
|
||||
<li>• Public keys (pubkeys) for each account</li>
|
||||
@@ -208,7 +211,7 @@ export function DashboardExport() {
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
disabled={isExporting || !contactListEvent}
|
||||
className="w-full"
|
||||
className="w-full rounded-none border-2 border-foreground font-black uppercase"
|
||||
size="lg"
|
||||
>
|
||||
<Download className="mr-2 h-5 w-5" />
|
||||
@@ -224,9 +227,9 @@ export function DashboardExport() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Card className="rounded-none border-2 border-foreground bg-card shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">About Your Following List</CardTitle>
|
||||
<CardTitle className="text-base font-black uppercase">About your following list</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground space-y-2">
|
||||
<p>
|
||||
|
||||
@@ -1,15 +1,87 @@
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Activity,
|
||||
ArrowDown,
|
||||
CheckCircle2,
|
||||
Copy,
|
||||
Globe2,
|
||||
Radio,
|
||||
Server,
|
||||
ShieldCheck,
|
||||
Users,
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { CheckCircle2, Copy, Server, Gift, Users, Globe } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
|
||||
const relayUrl = 'wss://relay.layer.systems';
|
||||
|
||||
const tickerItems = [
|
||||
'LAYER.systems',
|
||||
'public Nostr relay',
|
||||
'wss://relay.layer.systems',
|
||||
'open protocols',
|
||||
'relay-first social',
|
||||
'NIP-aware infrastructure',
|
||||
'bring your own client',
|
||||
'events over platforms',
|
||||
];
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Zap,
|
||||
title: 'Fast by design',
|
||||
description: 'A focused relay endpoint for clients that need clean access to the Nostr network.',
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: 'Open access',
|
||||
description: 'Free to use, easy to add, and built around portable Nostr identity instead of account lock-in.',
|
||||
},
|
||||
{
|
||||
icon: Globe2,
|
||||
title: 'Protocol native',
|
||||
description: 'Publishes and queries standard Nostr events so your client stays interoperable.',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Community useful',
|
||||
description: 'A practical relay surface for people, apps, and experiments building on decentralized social media.',
|
||||
},
|
||||
];
|
||||
|
||||
const connectionSteps = [
|
||||
{
|
||||
label: '01',
|
||||
title: 'Open your Nostr client',
|
||||
description: 'Use Damus, Amethyst, Snort, Coracle, or any client that lets you manage relays.',
|
||||
},
|
||||
{
|
||||
label: '02',
|
||||
title: 'Add the relay URL',
|
||||
description: 'Paste the relay endpoint into your read and write relay list.',
|
||||
},
|
||||
{
|
||||
label: '03',
|
||||
title: 'Publish and query',
|
||||
description: 'Your client can now send and receive events through LAYER.systems.',
|
||||
},
|
||||
];
|
||||
|
||||
const relayStats = [
|
||||
['Endpoint', relayUrl],
|
||||
['Access', 'public'],
|
||||
['Network', 'Nostr'],
|
||||
['Mode', 'read + write'],
|
||||
];
|
||||
|
||||
const Index = () => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const relayUrl = 'wss://relay.layer.systems';
|
||||
|
||||
useSeoMeta({
|
||||
title: 'LAYER.systems - Public Nostr Relay',
|
||||
@@ -21,7 +93,7 @@ const Index = () => {
|
||||
await navigator.clipboard.writeText(relayUrl);
|
||||
setCopied(true);
|
||||
toast({
|
||||
title: 'Copied!',
|
||||
title: 'Copied',
|
||||
description: 'Relay URL copied to clipboard',
|
||||
});
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
@@ -34,207 +106,181 @@ const Index = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Gift,
|
||||
title: 'Free',
|
||||
description: 'No cost to use - accessible for everyone',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Community driven',
|
||||
description: 'Built and maintained by the Nostr community',
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
title: 'Global Network',
|
||||
description: 'Part of the decentralized Nostr ecosystem',
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
title: 'Open Access',
|
||||
description: 'Free to use for all Nostr clients',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-background via-background to-muted/20">
|
||||
{/* Header */}
|
||||
<header className="absolute top-0 left-0 right-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex justify-end">
|
||||
<LoginArea className="max-w-60" />
|
||||
</div>
|
||||
<div className="min-h-screen overflow-hidden bg-background text-foreground">
|
||||
<header className="sticky top-0 z-50 border-b-2 border-foreground bg-background/95 backdrop-blur">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-3 sm:px-6 lg:px-8">
|
||||
<a href="#top" className="group flex min-w-0 items-center gap-3 font-black uppercase tracking-normal">
|
||||
<span className="grid h-9 w-9 place-items-center border-2 border-foreground bg-primary text-primary-foreground shadow-[4px_4px_0_hsl(var(--foreground))] transition-transform group-hover:-translate-y-0.5">
|
||||
<Radio className="h-5 w-5" />
|
||||
</span>
|
||||
<span className="hidden text-lg leading-none sm:inline sm:text-xl">LAYER.systems</span>
|
||||
</a>
|
||||
|
||||
<nav className="hidden items-center gap-6 text-sm font-bold uppercase md:flex">
|
||||
<a href="#relay" className="transition-colors hover:text-primary">Relay</a>
|
||||
<a href="#features" className="transition-colors hover:text-primary">Signal</a>
|
||||
<a href="#connect" className="transition-colors hover:text-primary">Connect</a>
|
||||
<Link to="/explore" className="transition-colors hover:text-primary">Explore</Link>
|
||||
</nav>
|
||||
|
||||
<LoginArea className="max-w-[12rem] sm:max-w-60" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<div className="relative overflow-hidden">
|
||||
{/* Animated Background Elements */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-1/4 -left-20 w-96 h-96 bg-primary/5 rounded-full blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-1/4 -right-20 w-96 h-96 bg-primary/5 rounded-full blur-3xl animate-pulse delay-1000" />
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-16 sm:pt-32 sm:pb-24">
|
||||
{/* Status Badge */}
|
||||
{/* <div className="flex justify-center mb-8">
|
||||
<Badge variant="outline" className="px-4 py-2 text-sm font-medium gap-2 border-primary/20 bg-primary/5">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
Relay Online
|
||||
</Badge>
|
||||
</div> */}
|
||||
|
||||
{/* Main Heading */}
|
||||
<div className="text-center space-y-6 mb-12">
|
||||
<h1 className="text-5xl sm:text-7xl font-bold tracking-tight">
|
||||
<span className="bg-gradient-to-r from-primary via-primary to-primary/60 bg-clip-text text-transparent">
|
||||
LAYER.systems
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xl sm:text-2xl text-muted-foreground max-w-2xl mx-auto font-light">
|
||||
Your gateway to the decentralized social network
|
||||
</p>
|
||||
<p className="text-base sm:text-lg text-muted-foreground/80 max-w-xl mx-auto">
|
||||
A fast, reliable, and open Nostr relay connecting you to the future of social media
|
||||
</p>
|
||||
<main id="top">
|
||||
<section className="border-b-2 border-foreground bg-primary text-primary-foreground">
|
||||
<div className="relay-ticker flex overflow-hidden py-2 text-sm font-black uppercase">
|
||||
<div className="relay-ticker-track flex min-w-full shrink-0 items-center gap-8 px-4">
|
||||
{[...tickerItems, ...tickerItems].map((item, index) => (
|
||||
<span key={`${item}-${index}`} className="whitespace-nowrap">
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div aria-hidden className="relay-ticker-track flex min-w-full shrink-0 items-center gap-8 px-4">
|
||||
{[...tickerItems, ...tickerItems].map((item, index) => (
|
||||
<span key={`clone-${item}-${index}`} className="whitespace-nowrap">
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Relay URL Card */}
|
||||
<div className="max-w-2xl mx-auto mb-16">
|
||||
<Card className="border-2 border-primary/20 shadow-2xl shadow-primary/5 backdrop-blur-sm bg-card/95">
|
||||
<CardContent className="p-8">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="sm:flex-1 min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-2">Relay URL</p>
|
||||
<code className="text-lg sm:text-xl font-mono text-primary break-all">
|
||||
{relayUrl}
|
||||
</code>
|
||||
</div>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={copyToClipboard}
|
||||
className="w-full sm:w-auto sm:shrink-0 gap-2 hover:scale-105 transition-transform"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckCircle2 className="w-5 h-5" />
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-5 h-5" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Add this URL to your Nostr client to connect to LAYER.systems
|
||||
<section className="relative border-b-2 border-foreground bg-[linear-gradient(90deg,hsl(var(--border))_1px,transparent_1px),linear-gradient(180deg,hsl(var(--border))_1px,transparent_1px)] bg-[size:32px_32px]">
|
||||
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-14 sm:px-6 sm:py-20 lg:grid-cols-[minmax(0,1.1fr)_minmax(340px,0.9fr)] lg:px-8 lg:py-24">
|
||||
<div className="flex min-w-0 flex-col justify-between gap-10">
|
||||
<div className="space-y-7">
|
||||
<div className="inline-flex border-2 border-foreground bg-card px-3 py-1 text-xs font-black uppercase shadow-[3px_3px_0_hsl(var(--primary))]">
|
||||
Public Nostr relay
|
||||
</div>
|
||||
<div className="space-y-5">
|
||||
<h1 className="max-w-4xl text-5xl font-black leading-[0.92] tracking-normal sm:text-7xl lg:text-8xl">
|
||||
Relay signal for the open social web.
|
||||
</h1>
|
||||
<p className="max-w-2xl text-lg font-medium leading-8 text-muted-foreground sm:text-xl">
|
||||
LAYER.systems is a public Nostr relay for people and clients that want direct,
|
||||
portable, protocol-native social infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">Why Choose LAYER.systems?</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{features.map((feature, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="group hover:border-primary/40 transition-all duration-300 hover:shadow-lg hover:shadow-primary/5 hover:-translate-y-1"
|
||||
>
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<div className="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
|
||||
<feature.icon className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">{feature.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{feature.description}</p>
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Button asChild size="lg" className="h-12 rounded-none border-2 border-foreground px-5 font-black uppercase shadow-[4px_4px_0_hsl(var(--foreground))] hover:-translate-y-0.5">
|
||||
<a href="#relay">
|
||||
Relay URL
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild size="lg" variant="outline" className="h-12 rounded-none border-2 border-foreground bg-card px-5 font-black uppercase shadow-[4px_4px_0_hsl(var(--primary))] hover:-translate-y-0.5">
|
||||
<a href="#connect">Connect client</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="relay" className="self-start lg:mt-12 border-2 border-foreground bg-card p-4 shadow-[8px_8px_0_hsl(var(--primary))]">
|
||||
<div className="mb-4 flex items-center justify-between gap-4 border-b-2 border-foreground pb-4">
|
||||
<div>
|
||||
<p className="text-xs font-black uppercase text-muted-foreground">Relay endpoint</p>
|
||||
<code className="mt-1 block break-all font-mono text-xl font-bold text-primary sm:text-2xl">
|
||||
{relayUrl}
|
||||
</code>
|
||||
</div>
|
||||
<Server className="h-8 w-8 shrink-0 text-primary" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={copyToClipboard}
|
||||
className="mb-5 h-12 w-full rounded-none border-2 border-foreground font-black uppercase"
|
||||
>
|
||||
{copied ? <CheckCircle2 className="h-5 w-5" /> : <Copy className="h-5 w-5" />}
|
||||
{copied ? 'Copied' : 'Copy relay URL'}
|
||||
</Button>
|
||||
|
||||
<dl className="grid grid-cols-2 gap-px overflow-hidden border-2 border-foreground bg-foreground">
|
||||
{relayStats.map(([label, value]) => (
|
||||
<div key={label} className="bg-card p-3">
|
||||
<dt className="text-xs font-black uppercase text-muted-foreground">{label}</dt>
|
||||
<dd className="mt-1 break-words font-mono text-sm font-bold">{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="features" className="border-b-2 border-foreground bg-card">
|
||||
<div className="mx-auto max-w-7xl px-4 py-14 sm:px-6 sm:py-20 lg:px-8">
|
||||
<div className="mb-10 grid gap-5 md:grid-cols-[0.8fr_1.2fr] md:items-end">
|
||||
<h2 className="text-4xl font-black leading-none sm:text-5xl">Built like relay infrastructure should feel.</h2>
|
||||
<p className="text-lg font-medium leading-8 text-muted-foreground">
|
||||
The interface puts the endpoint first: clear, copyable, and surrounded by just enough context
|
||||
to help humans and Nostr clients get moving.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{features.map((feature) => (
|
||||
<Card key={feature.title} className="rounded-none border-2 border-foreground shadow-none transition-transform hover:-translate-y-1">
|
||||
<CardContent className="p-5">
|
||||
<div className="mb-6 grid h-12 w-12 place-items-center border-2 border-foreground bg-primary text-primary-foreground">
|
||||
<feature.icon className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="mb-3 text-xl font-black">{feature.title}</h3>
|
||||
<p className="leading-7 text-muted-foreground">{feature.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How to Connect Section */}
|
||||
<div className="border-t border-border/40">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">Getting Started</h2>
|
||||
<div className="space-y-8">
|
||||
<Card>
|
||||
<CardContent className="p-8">
|
||||
<div className="space-y-6">
|
||||
<div className="flex gap-4">
|
||||
<div className="shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-semibold">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Choose Your Client</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Pick a Nostr client like Damus, Amethyst, Snort, or any other compatible application
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-semibold">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Add the Relay</h3>
|
||||
<p className="text-muted-foreground">
|
||||
In your client settings, add <code className="px-2 py-1 bg-muted rounded text-sm font-mono">{relayUrl}</code> to your relay list
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="shrink-0 w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-semibold">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Start Connecting</h3>
|
||||
<p className="text-muted-foreground">
|
||||
You're all set! Start posting, following, and connecting with the Nostr network
|
||||
</p>
|
||||
</div>
|
||||
<section id="connect" className="border-b-2 border-foreground bg-secondary">
|
||||
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-14 sm:px-6 sm:py-20 lg:grid-cols-[0.85fr_1.15fr] lg:px-8">
|
||||
<div>
|
||||
<div className="mb-5 inline-flex border-2 border-foreground bg-card px-3 py-1 text-xs font-black uppercase">
|
||||
Getting started
|
||||
</div>
|
||||
<h2 className="text-4xl font-black leading-none sm:text-5xl">Three steps, no platform ceremony.</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{connectionSteps.map((step) => (
|
||||
<div key={step.label} className="grid gap-4 border-2 border-foreground bg-card p-5 sm:grid-cols-[72px_1fr]">
|
||||
<div className="font-mono text-3xl font-black text-primary">{step.label}</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-black">{step.title}</h3>
|
||||
<p className="mt-2 leading-7 text-muted-foreground">
|
||||
{step.description}{' '}
|
||||
{step.label === '02' && (
|
||||
<code className="break-all bg-accent px-1.5 py-1 font-mono text-sm text-accent-foreground">
|
||||
{relayUrl}
|
||||
</code>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-border/40 bg-muted/20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} LAYER.systems. Powered by Nostr.
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Server className="w-4 h-4" />
|
||||
<span>Open and free for all</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<a href="/terms" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
Terms
|
||||
</a>
|
||||
<a href="/privacy" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
Privacy
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
<section className="bg-foreground text-background">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-6 px-4 py-10 sm:px-6 md:flex-row md:items-center md:justify-between lg:px-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-6 w-6 text-primary" />
|
||||
<p className="font-black uppercase">Open relay. Orange signal. Nostr native.</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4 text-sm font-bold">
|
||||
<Link to="/terms" className="hover:text-primary">Terms</Link>
|
||||
<Link to="/privacy" className="hover:text-primary">Privacy</Link>
|
||||
<Link to="/dashboard" className="hover:text-primary">Dashboard</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user