diff --git a/src/components/layouts/AppShell.tsx b/src/components/layouts/AppShell.tsx index a3eb309..946ba81 100644 --- a/src/components/layouts/AppShell.tsx +++ b/src/components/layouts/AppShell.tsx @@ -13,9 +13,10 @@ import { AppShellContext } from "./AppShellContext"; interface AppShellProps { children: ReactNode; + hideBottomBar?: boolean; } -export function AppShell({ children }: AppShellProps) { +export function AppShell({ children, hideBottomBar = false }: AppShellProps) { const [commandLauncherOpen, setCommandLauncherOpen] = useState(false); // Sync active account and fetch relay lists @@ -76,7 +77,7 @@ export function AppShell({ children }: AppShellProps) {
{children}
- + {!hideBottomBar && } ); diff --git a/src/components/pages/PreviewAddressPage.tsx b/src/components/pages/PreviewAddressPage.tsx new file mode 100644 index 0000000..23fe996 --- /dev/null +++ b/src/components/pages/PreviewAddressPage.tsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router"; +import { nip19 } from "nostr-tools"; +import type { AddressPointer } from "nostr-tools/nip19"; +import { Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +/** + * PreviewAddressPage - Redirect naddr identifiers to appropriate routes + * Route: /naddr... + * For spellbooks (kind 30777), redirects to /:actor/:identifier + * For other kinds, shows error (could be extended to handle other addressable events) + */ +export default function PreviewAddressPage() { + const { identifier } = useParams<{ identifier: string }>(); + const navigate = useNavigate(); + const [error, setError] = useState(null); + + useEffect(() => { + if (!identifier) { + setError("No identifier provided"); + return; + } + + // Reconstruct the full identifier + const fullIdentifier = `naddr${identifier}`; + + try { + const decoded = nip19.decode(fullIdentifier); + + if (decoded.type !== "naddr") { + setError(`Invalid identifier type: expected naddr, got ${decoded.type}`); + toast.error("Invalid naddr identifier"); + return; + } + + const pointer = decoded.data as AddressPointer; + + // Check if it's a spellbook (kind 30777) + if (pointer.kind === 30777) { + // Redirect to the spellbook route + const npub = nip19.npubEncode(pointer.pubkey); + navigate(`/${npub}/${pointer.identifier}`, { replace: true }); + } else { + // For other kinds, we could extend this to handle them differently + // For now, show an error + setError( + `Addressable events of kind ${pointer.kind} are not yet supported in preview mode` + ); + toast.error("Unsupported event kind"); + } + } catch (e) { + console.error("Failed to decode naddr:", e); + setError(e instanceof Error ? e.message : "Failed to decode identifier"); + toast.error("Invalid naddr identifier"); + } + }, [identifier, navigate]); + + // Loading/error state + if (error) { + return ( +
+
+ {error} +
+ +
+ ); + } + + return ( +
+ +
+

Redirecting...

+

Processing address pointer

+
+
+ ); +} diff --git a/src/components/pages/PreviewEventPage.tsx b/src/components/pages/PreviewEventPage.tsx new file mode 100644 index 0000000..53cff18 --- /dev/null +++ b/src/components/pages/PreviewEventPage.tsx @@ -0,0 +1,96 @@ +import { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router"; +import { nip19 } from "nostr-tools"; +import type { EventPointer } from "nostr-tools/nip19"; +import { EventDetailViewer } from "../EventDetailViewer"; +import { Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +/** + * PreviewEventPage - Preview a Nostr event from a nevent or note identifier + * Routes: /nevent..., /note... + * This page shows a single event view without affecting user's workspace layout + */ +export default function PreviewEventPage() { + const { identifier } = useParams<{ identifier: string }>(); + const navigate = useNavigate(); + const [pointer, setPointer] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (!identifier) { + setError("No identifier provided"); + return; + } + + // Determine the prefix based on the current path + const path = window.location.pathname; + let fullIdentifier: string; + + if (path.startsWith("/nevent")) { + fullIdentifier = `nevent${identifier}`; + } else if (path.startsWith("/note")) { + fullIdentifier = `note${identifier}`; + } else { + setError("Invalid route"); + return; + } + + try { + const decoded = nip19.decode(fullIdentifier); + + if (decoded.type === "nevent") { + setPointer(decoded.data); + } else if (decoded.type === "note") { + // note is just an event ID, convert to EventPointer + setPointer({ + id: decoded.data, + }); + } else { + setError(`Invalid identifier type: expected nevent or note, got ${decoded.type}`); + toast.error("Invalid event identifier"); + return; + } + } catch (e) { + console.error("Failed to decode event identifier:", e); + setError(e instanceof Error ? e.message : "Failed to decode identifier"); + toast.error("Invalid event identifier"); + } + }, [identifier]); + + // Loading state + if (!pointer && !error) { + return ( +
+ +
+

Loading Event...

+

Decoding identifier

+
+
+ ); + } + + // Error state + if (error) { + return ( +
+
+ {error} +
+ +
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/src/components/pages/PreviewProfilePage.tsx b/src/components/pages/PreviewProfilePage.tsx new file mode 100644 index 0000000..f153bb8 --- /dev/null +++ b/src/components/pages/PreviewProfilePage.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router"; +import { nip19 } from "nostr-tools"; +import { ProfileViewer } from "../ProfileViewer"; +import { Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +/** + * PreviewProfilePage - Preview a Nostr profile from an npub identifier + * Route: /npub... + * This page shows a single profile view without affecting user's workspace layout + */ +export default function PreviewProfilePage() { + const { identifier } = useParams<{ identifier: string }>(); + const navigate = useNavigate(); + const [pubkey, setPubkey] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (!identifier) { + setError("No identifier provided"); + return; + } + + // Reconstruct the full identifier (react-router splits on /) + const fullIdentifier = `npub${identifier}`; + + try { + const decoded = nip19.decode(fullIdentifier); + if (decoded.type !== "npub") { + setError(`Invalid identifier type: expected npub, got ${decoded.type}`); + toast.error("Invalid npub identifier"); + return; + } + + setPubkey(decoded.data); + } catch (e) { + console.error("Failed to decode npub:", e); + setError(e instanceof Error ? e.message : "Failed to decode identifier"); + toast.error("Invalid npub identifier"); + } + }, [identifier]); + + // Loading state + if (!pubkey && !error) { + return ( +
+ +
+

Loading Profile...

+

Decoding identifier

+
+
+ ); + } + + // Error state + if (error) { + return ( +
+
+ {error} +
+ +
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/src/root.tsx b/src/root.tsx index 86e569e..3cb5929 100644 --- a/src/root.tsx +++ b/src/root.tsx @@ -2,6 +2,9 @@ import { createBrowserRouter, RouterProvider } from "react-router"; import { AppShell } from "./components/layouts/AppShell"; import DashboardPage from "./components/pages/DashboardPage"; import SpellbookPage from "./components/pages/SpellbookPage"; +import PreviewProfilePage from "./components/pages/PreviewProfilePage"; +import PreviewEventPage from "./components/pages/PreviewEventPage"; +import PreviewAddressPage from "./components/pages/PreviewAddressPage"; const router = createBrowserRouter([ { @@ -12,6 +15,38 @@ const router = createBrowserRouter([ ), }, + { + path: "/npub:identifier", + element: ( + + + + ), + }, + { + path: "/nevent:identifier", + element: ( + + + + ), + }, + { + path: "/note:identifier", + element: ( + + + + ), + }, + { + path: "/naddr:identifier", + element: ( + + + + ), + }, { path: "/preview/:actor/:identifier", element: (