12 changed files with 463 additions and 15 deletions
@ -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"); |
||||
};
|
||||
@ -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, |
||||
}, |
||||
}; |
||||
};
|
||||
@ -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()} |
||||
@ -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
|
||||
}; |
||||
};
|
||||
@ -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> |
||||
Loading…
Reference in new issue