mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 07:27:23 +02:00
fix: use loader-based routing for NIP-19 identifiers in React Router v7
Previous attempts using wildcard routes didn't work properly in React Router v7. Solution: - Single /:identifier route with a loader that validates NIP-19 prefixes - Loader throws 404 if identifier doesn't start with npub1/note1/nevent1/naddr1 - Created Nip19PreviewRouter component that routes to correct preview page - Routes are properly ordered: /:identifier before /:actor/:identifier catch-all This ensures /npub107jk... routes to profile preview, not spellbook route. Benefits: - Simpler routing configuration (1 route vs 4 duplicate routes) - Proper validation via loader - Clean separation of concerns with router component - Works correctly in React Router v7
This commit is contained in:
34
src/components/pages/Nip19PreviewRouter.tsx
Normal file
34
src/components/pages/Nip19PreviewRouter.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useParams } from "react-router";
|
||||
import PreviewProfilePage from "./PreviewProfilePage";
|
||||
import PreviewEventPage from "./PreviewEventPage";
|
||||
import PreviewAddressPage from "./PreviewAddressPage";
|
||||
|
||||
/**
|
||||
* Nip19PreviewRouter - Routes to the appropriate preview component based on NIP-19 identifier type
|
||||
* Handles npub, note, nevent, and naddr identifiers
|
||||
*/
|
||||
export default function Nip19PreviewRouter() {
|
||||
const { identifier } = useParams<{ identifier: string }>();
|
||||
|
||||
if (!identifier) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-muted-foreground">No identifier provided</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Route based on identifier prefix
|
||||
if (identifier.startsWith("npub1")) {
|
||||
return <PreviewProfilePage />;
|
||||
} else if (identifier.startsWith("nevent1")) {
|
||||
return <PreviewEventPage />;
|
||||
} else if (identifier.startsWith("note1")) {
|
||||
return <PreviewEventPage />;
|
||||
} else if (identifier.startsWith("naddr1")) {
|
||||
return <PreviewAddressPage />;
|
||||
}
|
||||
|
||||
// Not a recognized NIP-19 identifier
|
||||
return null;
|
||||
}
|
||||
@@ -7,19 +7,16 @@ import { toast } from "sonner";
|
||||
|
||||
/**
|
||||
* PreviewAddressPage - Preview or redirect naddr identifiers
|
||||
* Route: /naddr1*
|
||||
* Route: /:identifier (where identifier starts with naddr1)
|
||||
* For spellbooks (kind 30777), redirects to /:actor/:identifier
|
||||
* For all other addressable events, shows detail view
|
||||
*/
|
||||
export default function PreviewAddressPage() {
|
||||
const params = useParams<{ "*": string }>();
|
||||
const { identifier } = useParams<{ identifier: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Get the full naddr from the URL (naddr1 + captured part)
|
||||
const fullIdentifier = params["*"] ? `naddr1${params["*"]}` : undefined;
|
||||
|
||||
// Decode the naddr identifier (synchronous, memoized)
|
||||
const { decoded, error } = useNip19Decode(fullIdentifier, "naddr");
|
||||
const { decoded, error } = useNip19Decode(identifier, "naddr");
|
||||
|
||||
// Handle redirect for spellbooks
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useEffect } from "react";
|
||||
import { useParams, useNavigate, useLocation } from "react-router";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { useNip19Decode } from "@/hooks/useNip19Decode";
|
||||
import type { EventPointer } from "nostr-tools/nip19";
|
||||
import { EventDetailViewer } from "../EventDetailViewer";
|
||||
@@ -7,30 +7,15 @@ import { toast } from "sonner";
|
||||
|
||||
/**
|
||||
* PreviewEventPage - Preview a Nostr event from a nevent or note identifier
|
||||
* Routes: /nevent1*, /note1*
|
||||
* Route: /:identifier (where identifier starts with nevent1 or note1)
|
||||
* This page shows a single event view without affecting user's workspace layout
|
||||
*/
|
||||
export default function PreviewEventPage() {
|
||||
const params = useParams<{ "*": string }>();
|
||||
const { identifier } = useParams<{ identifier: string }>();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// Determine the prefix based on the current path and reconstruct full identifier
|
||||
const fullIdentifier = useMemo(() => {
|
||||
const captured = params["*"];
|
||||
if (!captured) return undefined;
|
||||
|
||||
const path = location.pathname;
|
||||
if (path.startsWith("/nevent1")) {
|
||||
return `nevent1${captured}`;
|
||||
} else if (path.startsWith("/note1")) {
|
||||
return `note1${captured}`;
|
||||
}
|
||||
return undefined;
|
||||
}, [params, location.pathname]);
|
||||
|
||||
// Decode the event identifier (synchronous, memoized)
|
||||
const { decoded, error } = useNip19Decode(fullIdentifier);
|
||||
const { decoded, error } = useNip19Decode(identifier);
|
||||
|
||||
// Convert decoded entity to EventPointer
|
||||
const pointer: EventPointer | null = useMemo(() => {
|
||||
|
||||
@@ -6,18 +6,15 @@ import { toast } from "sonner";
|
||||
|
||||
/**
|
||||
* PreviewProfilePage - Preview a Nostr profile from an npub identifier
|
||||
* Route: /npub1*
|
||||
* Route: /:identifier (where identifier starts with npub1)
|
||||
* This page shows a single profile view without affecting user's workspace layout
|
||||
*/
|
||||
export default function PreviewProfilePage() {
|
||||
const params = useParams<{ "*": string }>();
|
||||
const { identifier } = useParams<{ identifier: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Get the full npub from the URL (npub1 + captured part)
|
||||
const fullIdentifier = params["*"] ? `npub1${params["*"]}` : undefined;
|
||||
|
||||
// Decode the npub identifier (synchronous, memoized)
|
||||
const { decoded, error } = useNip19Decode(fullIdentifier, "npub");
|
||||
const { decoded, error } = useNip19Decode(identifier, "npub");
|
||||
|
||||
// Show error toast when error occurs
|
||||
useEffect(() => {
|
||||
|
||||
62
src/root.tsx
62
src/root.tsx
@@ -2,9 +2,7 @@ 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";
|
||||
import Nip19PreviewRouter from "./components/pages/Nip19PreviewRouter";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -15,38 +13,6 @@ const router = createBrowserRouter([
|
||||
</AppShell>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/npub1*",
|
||||
element: (
|
||||
<AppShell hideBottomBar>
|
||||
<PreviewProfilePage />
|
||||
</AppShell>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/nevent1*",
|
||||
element: (
|
||||
<AppShell hideBottomBar>
|
||||
<PreviewEventPage />
|
||||
</AppShell>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/note1*",
|
||||
element: (
|
||||
<AppShell hideBottomBar>
|
||||
<PreviewEventPage />
|
||||
</AppShell>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/naddr1*",
|
||||
element: (
|
||||
<AppShell hideBottomBar>
|
||||
<PreviewAddressPage />
|
||||
</AppShell>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/preview/:actor/:identifier",
|
||||
element: (
|
||||
@@ -55,6 +21,32 @@ const router = createBrowserRouter([
|
||||
</AppShell>
|
||||
),
|
||||
},
|
||||
// NIP-19 identifier preview route - must come before /:actor/:identifier catch-all
|
||||
{
|
||||
path: "/:identifier",
|
||||
element: (
|
||||
<AppShell hideBottomBar>
|
||||
<Nip19PreviewRouter />
|
||||
</AppShell>
|
||||
),
|
||||
// Only match single-segment paths that look like NIP-19 identifiers
|
||||
loader: ({ params }) => {
|
||||
const id = params.identifier;
|
||||
if (
|
||||
!id ||
|
||||
!(
|
||||
id.startsWith("npub1") ||
|
||||
id.startsWith("note1") ||
|
||||
id.startsWith("nevent1") ||
|
||||
id.startsWith("naddr1")
|
||||
)
|
||||
) {
|
||||
throw new Response("Not Found", { status: 404 });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
// Catch-all for two-segment paths (spellbooks, etc.)
|
||||
{
|
||||
path: "/:actor/:identifier",
|
||||
element: (
|
||||
|
||||
Reference in New Issue
Block a user