Harden onion boot flow and deepen site surfaces
Add persistent onion key backup and restore, improve startup resilience, and flesh out the major site verticals with richer navigation, search coverage, and operator documentation. Made-with: Cursor
This commit is contained in:
150
components/HubChatterPublicStrip.tsx
Normal file
150
components/HubChatterPublicStrip.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
CYBERLUX_ENTRY_COOKIE,
|
||||
parseCyberluxEntry,
|
||||
type CyberluxEntry,
|
||||
} from "@/lib/cyberluxEntry";
|
||||
import { formatRingLabel } from "@/lib/forumRings";
|
||||
import { useAccount } from "@/contexts/AccountContext";
|
||||
import { getChatterSlice } from "@/lib/shopChatterThreads";
|
||||
|
||||
function readEntry(): CyberluxEntry {
|
||||
if (typeof document === "undefined") return "hub";
|
||||
const raw = `; ${document.cookie}`;
|
||||
const part = raw.split(`; ${CYBERLUX_ENTRY_COOKIE}=`);
|
||||
if (part.length !== 2) return "hub";
|
||||
const v = part.pop()?.split(";").shift();
|
||||
return parseCyberluxEntry(v);
|
||||
}
|
||||
|
||||
type HubSkin = { bar: string; accent: string };
|
||||
|
||||
const DEFAULT_HUB_SKIN: HubSkin = {
|
||||
bar: "border-zinc-700/40 bg-[#050608]/98 text-zinc-500",
|
||||
accent: "text-zinc-400",
|
||||
};
|
||||
|
||||
const SKINS: Partial<Record<CyberluxEntry, HubSkin>> = {
|
||||
hub: {
|
||||
bar: "border-cyan-500/25 bg-[#050608]/95 text-zinc-400",
|
||||
accent: "text-cyan-400/90",
|
||||
},
|
||||
forum: {
|
||||
bar: "border-[#5c4030] bg-[#090705]/98 text-[#b8a995]",
|
||||
accent: "text-[#ff9a3c]",
|
||||
},
|
||||
exchange: {
|
||||
bar: "border-[#4a3f36]/90 bg-[#0b0907]/98 text-[#a89888]",
|
||||
accent: "text-[#d4a574]",
|
||||
},
|
||||
wiki: {
|
||||
bar: "border-[#334155] bg-[#080c15]/98 text-[#94a3b8]",
|
||||
accent: "text-[#7dd3fc]",
|
||||
},
|
||||
market: {
|
||||
bar: "border-emerald-900/45 bg-[#030806]/98 text-[#9db0a4]",
|
||||
accent: "text-[#6ee7b7]",
|
||||
},
|
||||
barter: {
|
||||
bar: "border-rose-900/40 bg-[#0a0606]/98 text-[#c8a8a8]",
|
||||
accent: "text-[#fb7185]",
|
||||
},
|
||||
chatter: {
|
||||
bar: "border-violet-900/45 bg-[#070510]/98 text-[#a898c4]",
|
||||
accent: "text-[#c4b5fd]",
|
||||
},
|
||||
search: {
|
||||
bar: "border-slate-600/45 bg-[#050608]/98 text-[#94a3b8]",
|
||||
accent: "text-[#93c5fd]",
|
||||
},
|
||||
syndicate: {
|
||||
bar: "border-yellow-900/35 bg-[#0b0905]/98 text-[#b8a878]",
|
||||
accent: "text-[#facc15]",
|
||||
},
|
||||
w: {
|
||||
bar: "border-fuchsia-900/35 bg-[#08060a]/98 text-[#c4a8d4]",
|
||||
accent: "text-[#e879f9]",
|
||||
},
|
||||
};
|
||||
|
||||
function hubSkinFor(entry: CyberluxEntry): HubSkin {
|
||||
return SKINS[entry] ?? DEFAULT_HUB_SKIN;
|
||||
}
|
||||
|
||||
const PREVIEW_N = 3;
|
||||
|
||||
/**
|
||||
* Fixed strip above the disclosure bar: in-world “hub chatter” threads (forum links).
|
||||
* Hidden on `/chatter` where the full archive already fills the view.
|
||||
*/
|
||||
export default function HubChatterPublicStrip() {
|
||||
const pathname = usePathname();
|
||||
const { user, hydrated } = useAccount();
|
||||
const [entry, setEntry] = useState<CyberluxEntry>("hub");
|
||||
const threads = useMemo(() => getChatterSlice(1, PREVIEW_N).items, []);
|
||||
|
||||
useEffect(() => {
|
||||
setEntry(readEntry());
|
||||
}, []);
|
||||
|
||||
if (pathname?.startsWith("/chatter")) return null;
|
||||
|
||||
const skin = hubSkinFor(entry);
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`pointer-events-auto fixed bottom-9 left-0 right-0 z-[9997] border-t px-3 py-2 shadow-[0_-6px_20px_rgba(0,0,0,0.4)] backdrop-blur-md sm:px-4 ${skin.bar}`}
|
||||
aria-label="Public hub chatter — recent threads about the main storefront"
|
||||
>
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-x-2 gap-y-1 font-mono text-[9px] uppercase tracking-[0.22em]">
|
||||
<span className="rounded border border-white/10 px-1.5 py-0.5 text-[8px] text-[#94a3b8]/90">Public</span>
|
||||
<span className="opacity-50">·</span>
|
||||
<Link href="/chatter" className={`font-semibold ${skin.accent} hover:underline`}>
|
||||
Hub chatter
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ul className="flex min-w-0 flex-1 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-start sm:gap-x-6 sm:gap-y-1">
|
||||
{threads.map((t, i) => (
|
||||
<li
|
||||
key={t.id}
|
||||
className={`min-w-0 sm:max-w-[min(100%,280px)] ${i > 0 ? "hidden sm:block" : ""}`}
|
||||
>
|
||||
<Link
|
||||
href={`/forum/post/${t.id}`}
|
||||
className="group block font-mono text-[11px] leading-snug text-[#c8b8a4] hover:text-[#ffcb8a]"
|
||||
>
|
||||
<span className={`mr-1.5 text-[9px] uppercase tracking-wider ${skin.accent} opacity-85`}>
|
||||
{formatRingLabel(t.topicSlug)}
|
||||
</span>
|
||||
<span className="line-clamp-2 sm:line-clamp-1">{t.title}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-3 font-mono text-[10px] uppercase tracking-wider">
|
||||
<Link href="/chatter" className={`${skin.accent} hover:underline`}>
|
||||
Full archive →
|
||||
</Link>
|
||||
<Link href="/forum" className="text-[#6b6358] hover:text-[#b8a995]">
|
||||
Forum
|
||||
</Link>
|
||||
{hydrated ? (
|
||||
<Link
|
||||
href={user ? "/dashboard" : "/sign-in"}
|
||||
className="text-[#6b6358] hover:text-[#b8a995]"
|
||||
>
|
||||
{user ? "Dashboard" : "Sign in"}
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user