Browse Source

AI - refactor for route params

Needs developer review
master
buttercat1791 8 months ago
parent
commit
b665e1b019
  1. 17
      src/lib/components/publications/PublicationHeader.svelte
  2. 4
      src/lib/components/util/ContainingIndexes.svelte
  3. 2
      src/lib/components/util/ViewPublicationLink.svelte
  4. 2
      src/lib/navigator/EventNetwork/NodeTooltip.svelte
  5. 36
      src/lib/utils.ts
  6. 4
      src/routes/about/+page.svelte
  7. 25
      src/routes/publication/+page.server.ts
  8. 132
      src/routes/publication/[type]/[identifier]/+layout.server.ts
  9. 28
      src/routes/publication/[type]/[identifier]/+layout.svelte
  10. 123
      src/routes/publication/[type]/[identifier]/+page.server.ts
  11. 95
      src/routes/publication/[type]/[identifier]/+page.svelte
  12. 10
      src/routes/start/+page.svelte

17
src/lib/components/publications/PublicationHeader.svelte

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
<script lang="ts">
import { naddrEncode } from "$lib/utils";
import { naddrEncode, neventEncode } from "$lib/utils";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { activeInboxRelays } from "$lib/ndk";
import { Card } from "flowbite-svelte";
@ -20,10 +20,19 @@ @@ -20,10 +20,19 @@
const href = $derived.by(() => {
const d = event.getMatchingTags("d")[0]?.[1];
if (d != null) {
return `publication?d=${d}`;
const isReplaceableEvent = event.kind === 30040 || event.kind === 30041;
if (d != null && isReplaceableEvent) {
// For replaceable events with d tag, use naddr encoding
const naddr = naddrEncode(event, relays);
return `publication/naddr/${naddr}`;
} else if (event.id) {
// For non-replaceable events or events without d tag, use nevent encoding
const nevent = neventEncode(event, relays);
return `publication/nevent/${nevent}`;
} else {
return `publication?id=${naddrEncode(event, relays)}`;
// Fallback to d tag if available
return d ? `publication/d/${d}` : null;
}
});

4
src/lib/components/util/ContainingIndexes.svelte

@ -47,12 +47,12 @@ @@ -47,12 +47,12 @@
function navigateToIndex(indexEvent: NDKEvent) {
const dTag = getMatchingTags(indexEvent, "d")[0]?.[1];
if (dTag) {
goto(`/publication?d=${encodeURIComponent(dTag)}`);
goto(`/publication/d/${encodeURIComponent(dTag)}`);
} else {
// Fallback to naddr
try {
const naddr = naddrEncode(indexEvent, $activeInboxRelays);
goto(`/publication?id=${encodeURIComponent(naddr)}`);
goto(`/publication/naddr/${encodeURIComponent(naddr)}`);
} catch (err) {
console.error("[ContainingIndexes] Error creating naddr:", err);
}

2
src/lib/components/util/ViewPublicationLink.svelte

@ -64,7 +64,7 @@ @@ -64,7 +64,7 @@
"ViewPublicationLink: Navigating to publication:",
naddrAddress,
);
goto(`/publication?id=${encodeURIComponent(naddrAddress)}`);
goto(`/publication/naddr/${encodeURIComponent(naddrAddress)}`);
} else {
console.log("ViewPublicationLink: No naddr address found for event");
}

2
src/lib/navigator/EventNetwork/NodeTooltip.svelte

@ -145,7 +145,7 @@ @@ -145,7 +145,7 @@
<div class="tooltip-content">
<!-- Title with link -->
<div class="tooltip-title">
<a href="/publication?id={node.id}" class="tooltip-title-link">
<a href="/publication/id/{node.id}" class="tooltip-title-link">
{node.title || "Untitled"}
</a>
</div>

36
src/lib/utils.ts

@ -29,6 +29,42 @@ export function nprofileEncode(pubkey: string, relays: string[]) { @@ -29,6 +29,42 @@ export function nprofileEncode(pubkey: string, relays: string[]) {
return nip19.nprofileEncode({ pubkey, relays });
}
/**
* Decodes an naddr identifier and returns the decoded data
*/
export function naddrDecode(naddr: string) {
try {
if (!naddr.startsWith('naddr')) {
throw new Error('Invalid naddr format');
}
const decoded = nip19.decode(naddr);
if (decoded.type !== 'naddr') {
throw new Error('Decoded result is not an naddr');
}
return decoded.data;
} catch (error) {
throw new Error(`Failed to decode naddr: ${error}`);
}
}
/**
* Decodes an nevent identifier and returns the decoded data
*/
export function neventDecode(nevent: string) {
try {
if (!nevent.startsWith('nevent')) {
throw new Error('Invalid nevent format');
}
const decoded = nip19.decode(nevent);
if (decoded.type !== 'nevent') {
throw new Error('Decoded result is not an nevent');
}
return decoded.data;
} catch (error) {
throw new Error(`Failed to decode nevent: ${error}`);
}
}
export function formatDate(unixtimestamp: number) {
const months = [
"Jan",

4
src/routes/about/+page.svelte

@ -26,11 +26,11 @@ @@ -26,11 +26,11 @@
<P class="mb-3">
Alexandria is a reader and writer for <A
href="./publication?d=gitcitadel-project-documentation-curated-publications-specification-7-by-stella-v-1"
href="./publication/d/gitcitadel-project-documentation-curated-publications-specification-7-by-stella-v-1"
>curated publications</A
> (in Asciidoc), wiki pages (Asciidoc), and will eventually also support long-form
articles (markup). It is produced by the <A
href="./publication?d=gitcitadel-project-documentation-by-stella-v-1"
href="./publication/d/gitcitadel-project-documentation-by-stella-v-1"
>GitCitadel project team</A
>.
</P>

25
src/routes/publication/+page.server.ts

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
import { redirect } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ url }) => {
const id = url.searchParams.get("id");
const dTag = url.searchParams.get("d");
// Handle backward compatibility for old query-based routes
if (id) {
// Check if id is an naddr or nevent
if (id.startsWith("naddr")) {
throw redirect(301, `/publication/naddr/${id}`);
} else if (id.startsWith("nevent")) {
throw redirect(301, `/publication/nevent/${id}`);
} else {
// Assume it's a hex ID
throw redirect(301, `/publication/id/${id}`);
}
} else if (dTag) {
throw redirect(301, `/publication/d/${dTag}`);
}
// If no query parameters, redirect to the start page or show publication feed
throw redirect(301, "/start");
};

132
src/routes/publication/[type]/[identifier]/+layout.server.ts

@ -0,0 +1,132 @@ @@ -0,0 +1,132 @@
import { error } from "@sveltejs/kit";
import type { LayoutServerLoad } from "./$types";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { getActiveRelaySetAsNDKRelaySet } from "../../../../lib/ndk.ts";
import { getMatchingTags } from "../../../../lib/utils/nostrUtils.ts";
import { naddrDecode, neventDecode } from "../../../../lib/utils.ts";
import type NDK from "@nostr-dev-kit/ndk";
/**
* Fetches an event by hex ID
*/
async function fetchEventById(ndk: NDK, id: string): Promise<NDKEvent> {
try {
const event = await ndk.fetchEvent(id);
if (!event) {
throw new Error(`Event not found for ID: ${id}`);
}
return event;
} catch (err) {
throw error(404, `Failed to fetch publication root event.\n${err}`);
}
}
/**
* Fetches an event by d tag
*/
async function fetchEventByDTag(ndk: NDK, dTag: string): Promise<NDKEvent> {
try {
const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true);
const events = await ndk.fetchEvents(
{ "#d": [dTag] },
{ closeOnEose: false },
relaySet,
);
if (!events || events.size === 0) {
throw new Error(`Event not found for d tag: ${dTag}`);
}
// Choose the event with the latest created_at timestamp when multiple events share the same d tag
const sortedEvents = Array.from(events).sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
return sortedEvents[0];
} catch (err) {
throw error(404, `Failed to fetch publication root event.\n${err}`);
}
}
/**
* Fetches an event by naddr identifier
*/
async function fetchEventByNaddr(ndk: NDK, naddr: string): Promise<NDKEvent> {
try {
const decoded = naddrDecode(naddr);
const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true);
const filter = {
kinds: [decoded.kind],
authors: [decoded.pubkey],
"#d": [decoded.identifier],
};
const event = await ndk.fetchEvent(filter, { closeOnEose: false }, relaySet);
if (!event) {
throw new Error(`Event not found for naddr: ${naddr}`);
}
return event;
} catch (err) {
throw error(404, `Failed to fetch publication root event.\n${err}`);
}
}
/**
* Fetches an event by nevent identifier
*/
async function fetchEventByNevent(ndk: NDK, nevent: string): Promise<NDKEvent> {
try {
const decoded = neventDecode(nevent);
const event = await ndk.fetchEvent(decoded.id);
if (!event) {
throw new Error(`Event not found for nevent: ${nevent}`);
}
return event;
} catch (err) {
throw error(404, `Failed to fetch publication root event.\n${err}`);
}
}
export const load: LayoutServerLoad = async ({ params, parent, url }) => {
const { type, identifier } = params;
const { ndk } = await parent();
if (!ndk) {
throw error(500, "NDK not available");
}
let indexEvent: NDKEvent;
// Handle different identifier types
switch (type) {
case 'id':
indexEvent = await fetchEventById(ndk, identifier);
break;
case 'd':
indexEvent = await fetchEventByDTag(ndk, identifier);
break;
case 'naddr':
indexEvent = await fetchEventByNaddr(ndk, identifier);
break;
case 'nevent':
indexEvent = await fetchEventByNevent(ndk, identifier);
break;
default:
throw error(400, `Unsupported identifier type: ${type}`);
}
// Extract metadata for meta tags
const title = getMatchingTags(indexEvent, "title")[0]?.[1] || "Alexandria Publication";
const summary = getMatchingTags(indexEvent, "summary")[0]?.[1] ||
"Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.";
const image = getMatchingTags(indexEvent, "image")[0]?.[1] || "/screenshots/old_books.jpg";
const currentUrl = `${url.origin}${url.pathname}`;
return {
indexEvent,
metadata: {
title,
summary,
image,
currentUrl,
},
};
};

28
src/routes/publication/[type]/[identifier]/+layout.svelte

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
<script lang="ts">
import type { LayoutProps } from "./$types";
let { data, children }: LayoutProps = $props();
const { metadata } = data;
</script>
<svelte:head>
<!-- Basic meta tags -->
<title>{metadata.title}</title>
<meta name="description" content={metadata.summary} />
<!-- OpenGraph meta tags -->
<meta property="og:title" content={metadata.title} />
<meta property="og:description" content={metadata.summary} />
<meta property="og:url" content={metadata.currentUrl} />
<meta property="og:type" content="article" />
<meta property="og:site_name" content="Alexandria" />
<meta property="og:image" content={metadata.image} />
<!-- Twitter Card meta tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={metadata.title} />
<meta name="twitter:description" content={metadata.summary} />
<meta name="twitter:image" content={metadata.image} />
</svelte:head>
{@render children()}

123
src/routes/publication/[type]/[identifier]/+page.server.ts

@ -0,0 +1,123 @@ @@ -0,0 +1,123 @@
import { error } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { getActiveRelaySetAsNDKRelaySet } from "../../../../lib/ndk.ts";
import { getMatchingTags } from "../../../../lib/utils/nostrUtils.ts";
import { naddrDecode, neventDecode } from "../../../../lib/utils.ts";
import type NDK from "@nostr-dev-kit/ndk";
/**
* Fetches an event by hex ID
*/
async function fetchEventById(ndk: NDK, id: string): Promise<NDKEvent> {
try {
const event = await ndk.fetchEvent(id);
if (!event) {
throw new Error(`Event not found for ID: ${id}`);
}
return event;
} catch (err) {
throw error(404, `Failed to fetch publication root event.\n${err}`);
}
}
/**
* Fetches an event by d tag
*/
async function fetchEventByDTag(ndk: NDK, dTag: string): Promise<NDKEvent> {
try {
const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true); // true for inbox relays
const events = await ndk.fetchEvents(
{ "#d": [dTag] },
{ closeOnEose: false },
relaySet,
);
if (!events || events.size === 0) {
throw new Error(`Event not found for d tag: ${dTag}`);
}
// AI-NOTE: Choose the event with the latest created_at timestamp when multiple events share the same d tag
const sortedEvents = Array.from(events).sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
return sortedEvents[0];
} catch (err) {
throw error(404, `Failed to fetch publication root event.\n${err}`);
}
}
/**
* Fetches an event by naddr identifier
*/
async function fetchEventByNaddr(ndk: NDK, naddr: string): Promise<NDKEvent> {
try {
const decoded = naddrDecode(naddr);
const relaySet = await getActiveRelaySetAsNDKRelaySet(ndk, true);
const filter = {
kinds: [decoded.kind],
authors: [decoded.pubkey],
"#d": [decoded.identifier],
};
const event = await ndk.fetchEvent(filter, { closeOnEose: false }, relaySet);
if (!event) {
throw new Error(`Event not found for naddr: ${naddr}`);
}
return event;
} catch (err) {
throw error(404, `Failed to fetch publication root event.\n${err}`);
}
}
/**
* Fetches an event by nevent identifier
*/
async function fetchEventByNevent(ndk: NDK, nevent: string): Promise<NDKEvent> {
try {
const decoded = neventDecode(nevent);
const event = await ndk.fetchEvent(decoded.id);
if (!event) {
throw new Error(`Event not found for nevent: ${nevent}`);
}
return event;
} catch (err) {
throw error(404, `Failed to fetch publication root event.\n${err}`);
}
}
export const load: PageServerLoad = async ({ params, parent }) => {
const { type, identifier } = params;
const { ndk } = await parent();
if (!ndk) {
throw error(500, "NDK not available");
}
let indexEvent: NDKEvent;
// Handle different identifier types
switch (type) {
case 'id':
indexEvent = await fetchEventById(ndk, identifier);
break;
case 'd':
indexEvent = await fetchEventByDTag(ndk, identifier);
break;
case 'naddr':
indexEvent = await fetchEventByNaddr(ndk, identifier);
break;
case 'nevent':
indexEvent = await fetchEventByNevent(ndk, identifier);
break;
default:
throw error(400, `Unsupported identifier type: ${type}`);
}
const publicationType = getMatchingTags(indexEvent, "type")[0]?.[1];
return {
publicationType,
indexEvent,
ndk, // Pass ndk to the page for the publication tree
};
};

95
src/routes/publication/[type]/[identifier]/+page.svelte

@ -0,0 +1,95 @@ @@ -0,0 +1,95 @@
<script lang="ts">
import Publication from "$lib/components/publications/Publication.svelte";
import { TextPlaceholder } from "flowbite-svelte";
import type { PageProps } from "./$types";
import { onDestroy, onMount, setContext } from "svelte";
import Processor from "asciidoctor";
import ArticleNav from "$components/util/ArticleNav.svelte";
import { SveltePublicationTree } from "$lib/components/publications/svelte_publication_tree.svelte";
import { TableOfContents } from "$lib/components/publications/table_of_contents.svelte";
import { page } from "$app/state";
import { goto } from "$app/navigation";
let { data }: PageProps = $props();
const publicationTree = new SveltePublicationTree(data.indexEvent, data.ndk);
const toc = new TableOfContents(
data.indexEvent.tagAddress(),
publicationTree,
page.url.pathname ?? "",
);
setContext("publicationTree", publicationTree);
setContext("toc", toc);
setContext("asciidoctor", Processor());
publicationTree.onBookmarkMoved((address) => {
goto(`#${address}`, {
replaceState: true,
});
// TODO: Extract IndexedDB interaction to a service layer.
// Store bookmark in IndexedDB
const db = indexedDB.open("alexandria", 1);
db.onupgradeneeded = () => {
const objectStore = db.result.createObjectStore("bookmarks", {
keyPath: "key",
});
};
db.onsuccess = () => {
const transaction = db.result.transaction(["bookmarks"], "readwrite");
const store = transaction.objectStore("bookmarks");
const bookmarkKey = `${data.indexEvent.tagAddress()}`;
store.put({ key: bookmarkKey, address });
};
});
onMount(() => {
// TODO: Extract IndexedDB interaction to a service layer.
// Read bookmark from IndexedDB
const db = indexedDB.open("alexandria", 1);
db.onupgradeneeded = () => {
const objectStore = db.result.createObjectStore("bookmarks", {
keyPath: "key",
});
};
db.onsuccess = () => {
const transaction = db.result.transaction(["bookmarks"], "readonly");
const store = transaction.objectStore("bookmarks");
const bookmarkKey = `${data.indexEvent.tagAddress()}`;
const request = store.get(bookmarkKey);
request.onsuccess = () => {
if (request.result?.address) {
// Set the bookmark in the publication tree
publicationTree.setBookmark(request.result.address);
// Jump to the bookmarked element
goto(`#${request.result.address}`, {
replaceState: true,
});
}
};
};
});
onDestroy(() => {
// TODO: Clean up resources if needed
});
</script>
<ArticleNav
publicationType={data.publicationType}
rootId={data.indexEvent.id}
indexEvent={data.indexEvent}
/>
<main class="publication {data.publicationType}">
<Publication
rootAddress={data.indexEvent.tagAddress()}
publicationType={data.publicationType}
indexEvent={data.indexEvent}
/>
</main>

10
src/routes/start/+page.svelte

@ -91,7 +91,7 @@ @@ -91,7 +91,7 @@
<P class="mb-3">
An example of a book is <a
href="/publication?d=jane-eyre-an-autobiography-by-charlotte-bront%C3%AB-v-3rd-edition"
href="/publication/d/jane-eyre-an-autobiography-by-charlotte-bront%C3%AB-v-3rd-edition"
>Jane Eyre</a
>
</P>
@ -127,7 +127,7 @@ @@ -127,7 +127,7 @@
<P class="mb-3">
An example of a research paper is <a
href="/publication?d=less-partnering-less-children-or-both-by-julia-hellstrand-v-1"
href="/publication/d/less-partnering-less-children-or-both-by-julia-hellstrand-v-1"
>Less Partnering, Less Children, or Both?</a
>
</P>
@ -145,9 +145,9 @@ @@ -145,9 +145,9 @@
<P class="mb-3">
Our own team uses Alexandria to document the app, to display our <a
href="/publication?d=the-gitcitadel-blog-by-stella-v-1">blog entries</a
href="/publication/d/the-gitcitadel-blog-by-stella-v-1">blog entries</a
>, as well as to store copies of our most interesting
<a href="/publication?d=gitcitadel-project-documentation-by-stella-v-1"
<a href="/publication/d/gitcitadel-project-documentation-by-stella-v-1"
>technical specifications</a
>.
</P>
@ -168,7 +168,7 @@ @@ -168,7 +168,7 @@
collaborative knowledge bases and documentation. Wiki pages, such as this
one about the <button
class="underline text-primary-700 bg-transparent border-none p-0"
onclick={() => goto("/publication?d=sybil")}>Sybil utility</button
onclick={() => goto("/publication/d/sybil")}>Sybil utility</button
> use the same Asciidoc format as other publications but are specifically designed
for interconnected, evolving content.
</P>

Loading…
Cancel
Save