Browse Source

Merge remote-tracking branch 'origin/master' into Issue#162

master
Silberengel 11 months ago
parent
commit
6ec8ac45be
  1. 2
      src/app.html
  2. 9
      src/lib/components/Publication.svelte
  3. 4
      src/lib/components/util/CardActions.svelte
  4. 126
      src/lib/navigator/EventNetwork/NodeTooltip.svelte
  5. 18
      src/lib/navigator/EventNetwork/index.svelte
  6. 29
      src/routes/+layout.svelte
  7. 5
      src/routes/about/+page.svelte
  8. 48
      src/routes/publication/+page.svelte
  9. 5
      src/routes/publication/+page.ts
  10. 67
      src/routes/visualize/+page.svelte
  11. BIN
      static/favicon.png
  12. BIN
      static/screenshots/old_books.jpg

2
src/app.html

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png?v=2" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
%sveltekit.head% %sveltekit.head%
</head> </head>

9
src/lib/components/Publication.svelte

@ -14,8 +14,14 @@
import Preview from "./Preview.svelte"; import Preview from "./Preview.svelte";
import { pharosInstance } from "$lib/parser"; import { pharosInstance } from "$lib/parser";
import { page } from "$app/state"; import { page } from "$app/state";
import { ndkInstance } from "$lib/ndk";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
let { rootId, publicationType } = $props<{ rootId: string, publicationType: string }>(); let { rootId, publicationType, indexEvent } = $props<{
rootId: string,
publicationType: string,
indexEvent: NDKEvent
}>();
if (rootId !== $pharosInstance.getRootIndexId()) { if (rootId !== $pharosInstance.getRootIndexId()) {
console.error("Root ID does not match parser root index ID"); console.error("Root ID does not match parser root index ID");
@ -94,6 +100,7 @@
}); });
</script> </script>
{#if showTocButton && !showToc} {#if showTocButton && !showToc}
<Button <Button
class="btn-leather fixed top-20 left-4 h-6 w-6" class="btn-leather fixed top-20 left-4 h-6 w-6"

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

@ -138,8 +138,8 @@
</Popover> </Popover>
{/if} {/if}
<!-- Event JSON --> <!-- Event JSON -->
<Modal class='modal-leather' title='Event JSON' bind:open={jsonModalOpen} autoclose outsideclose size='sm'> <Modal class='modal-leather' title='Event JSON' bind:open={jsonModalOpen} autoclose outsideclose size='lg'>
<div class="overflow-auto bg-highlight dark:bg-primary-900 text-sm rounded p-1"> <div class="overflow-auto bg-highlight dark:bg-primary-900 text-sm rounded p-1" style="max-height: 70vh;">
<pre><code>{JSON.stringify(event.rawEvent(), null, 2)}</code></pre> <pre><code>{JSON.stringify(event.rawEvent(), null, 2)}</code></pre>
</div> </div>
</Modal> </Modal>

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

@ -1,38 +1,128 @@
<script lang="ts"> <script lang="ts">
import type { NetworkNode } from "./types"; import type { NetworkNode } from "./types";
import { onMount, createEventDispatcher } from "svelte";
export let node: NetworkNode; let { node, selected = false, x, y } = $props<{
export let selected: boolean = false; node: NetworkNode;
export let x: number; selected?: boolean;
export let y: number; x: number;
y: number;
}>();
const dispatch = createEventDispatcher();
let tooltipElement: HTMLDivElement;
let tooltipX = $state(x + 10);
let tooltipY = $state(y - 10);
function getAuthorTag(node: NetworkNode): string {
if (node.event) {
const authorTags = node.event.getMatchingTags("author");
if (authorTags.length > 0) {
return authorTags[0][1];
}
}
return "Unknown";
}
function getSummaryTag(node: NetworkNode): string | null {
if (node.event) {
const summaryTags = node.event.getMatchingTags("summary");
if (summaryTags.length > 0) {
return summaryTags[0][1];
}
}
return null;
}
function getDTag(node: NetworkNode): string {
if (node.event) {
const dTags = node.event.getMatchingTags("d");
if (dTags.length > 0) {
return dTags[0][1];
}
}
return "View Publication";
}
function truncateContent(content: string, maxLength: number = 200): string {
if (content.length <= maxLength) return content;
return content.substring(0, maxLength) + "...";
}
function closeTooltip() {
dispatch('close');
}
// Ensure tooltip is fully visible on screen
onMount(() => {
if (tooltipElement) {
const rect = tooltipElement.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// Check if tooltip goes off the right edge
if (rect.right > windowWidth) {
tooltipX = windowWidth - rect.width - 10;
}
// Check if tooltip goes off the bottom edge
if (rect.bottom > windowHeight) {
tooltipY = windowHeight - rect.height - 10;
}
// Check if tooltip goes off the left edge
if (rect.left < 0) {
tooltipX = 10;
}
// Check if tooltip goes off the top edge
if (rect.top < 0) {
tooltipY = 10;
}
}
});
</script> </script>
<div <div
bind:this={tooltipElement}
class="tooltip-leather fixed p-4 rounded shadow-lg bg-primary-0 dark:bg-primary-800 class="tooltip-leather fixed p-4 rounded shadow-lg bg-primary-0 dark:bg-primary-800
border border-gray-200 dark:border-gray-800 transition-colors duration-200" border border-gray-200 dark:border-gray-800 transition-colors duration-200"
style="left: {x + 10}px; top: {y - 10}px; z-index: 1000;" style="left: {tooltipX}px; top: {tooltipY}px; z-index: 1000; max-width: 400px;"
> >
<div class="space-y-2"> <button
<div class="font-bold text-base">{node.title}</div> class="absolute top-2 left-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-full p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
onclick={closeTooltip}
aria-label="Close"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
<div class="space-y-2 pl-6">
<div class="font-bold text-base">
<a href="/publication?id={node.id}" class="text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500">
{node.title}
</a>
</div>
<div class="text-gray-600 dark:text-gray-400 text-sm"> <div class="text-gray-600 dark:text-gray-400 text-sm">
{node.type} ({node.kind}) {node.type} ({node.kind})
</div> </div>
<div <div class="text-gray-600 dark:text-gray-400 text-sm">
class="text-gray-600 dark:text-gray-400 text-sm overflow-hidden text-ellipsis" Author: {getAuthorTag(node)}
>
ID: {node.id}
{#if node.naddr}
<div>{node.naddr}</div>
{/if}
{#if node.nevent}
<div>{node.nevent}</div>
{/if}
</div> </div>
{#if node.isContainer && getSummaryTag(node)}
<div class="mt-2 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-auto max-h-40">
<span class="font-semibold">Summary:</span> {truncateContent(getSummaryTag(node) || "", 200)}
</div>
{/if}
{#if node.content} {#if node.content}
<div <div
class="mt-2 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-auto max-h-40" class="mt-2 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-auto max-h-40"
> >
{node.content} {truncateContent(node.content)}
</div> </div>
{/if} {/if}
{#if selected} {#if selected}

18
src/lib/navigator/EventNetwork/index.svelte

@ -8,6 +8,7 @@
import { createSimulation, setupDragHandlers, applyGlobalLogGravity, applyConnectedGravity } from "./utils/forceSimulation"; import { createSimulation, setupDragHandlers, applyGlobalLogGravity, applyConnectedGravity } from "./utils/forceSimulation";
import Legend from "./Legend.svelte"; import Legend from "./Legend.svelte";
import NodeTooltip from "./NodeTooltip.svelte"; import NodeTooltip from "./NodeTooltip.svelte";
import type { NetworkNode, NetworkLink } from "./types";
let { events = [] } = $props<{ events?: NDKEvent[] }>(); let { events = [] } = $props<{ events?: NDKEvent[] }>();
@ -90,14 +91,14 @@
function updateGraph() { function updateGraph() {
if (!svg || !events?.length || !svgGroup) return; if (!svg || !events?.length || !svgGroup) return;
const { nodes, links } = generateGraph(events, currentLevels); const { nodes, links } = generateGraph(events, Number(currentLevels));
if (!nodes.length) return; if (!nodes.length) return;
// Stop any existing simulation // Stop any existing simulation
if (simulation) simulation.stop(); if (simulation) simulation.stop();
// Create new simulation // Create new simulation
simulation = createSimulation(nodes, links, nodeRadius, linkDistance); simulation = createSimulation(nodes, links, Number(nodeRadius), Number(linkDistance));
const dragHandler = setupDragHandlers(simulation); const dragHandler = setupDragHandlers(simulation);
// Update links // Update links
@ -303,6 +304,11 @@
updateGraph(); updateGraph();
} }
}); });
function handleTooltipClose() {
tooltipVisible = false;
selectedNodeId = null;
}
</script> </script>
<div <div
@ -321,15 +327,9 @@
selected={tooltipNode.id === selectedNodeId} selected={tooltipNode.id === selectedNodeId}
x={tooltipX} x={tooltipX}
y={tooltipY} y={tooltipY}
on:close={handleTooltipClose}
/> />
{/if} {/if}
<Legend /> <Legend />
</div> </div>
<style>
.tooltip {
max-width: 300px;
word-wrap: break-word;
}
</style>

29
src/routes/+layout.svelte

@ -2,15 +2,44 @@
import "../app.css"; import "../app.css";
import Navigation from "$lib/components/Navigation.svelte"; import Navigation from "$lib/components/Navigation.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { page } from "$app/stores";
// Compute viewport height. // Compute viewport height.
$: displayHeight = window.innerHeight; $: displayHeight = window.innerHeight;
// Get standard metadata for OpenGraph tags
let title = 'Library of Alexandria';
let currentUrl = $page.url.href;
// Get default image and summary for the Alexandria website
let image = '/screenshots/old_books.jpg';
let summary = 'Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.';
onMount(() => { onMount(() => {
document.body.style.height = `${displayHeight}px`; document.body.style.height = `${displayHeight}px`;
}); });
</script> </script>
<svelte:head>
<!-- Basic meta tags -->
<title>{title}</title>
<meta name="description" content="{summary}" />
<!-- OpenGraph meta tags -->
<meta property="og:title" content="{title}" />
<meta property="og:description" content="{summary}" />
<meta property="og:url" content="{currentUrl}" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Alexandria" />
<meta property="og:image" content="{image}" />
<!-- Twitter Card meta tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{title}" />
<meta name="twitter:description" content="{summary}" />
<meta name="twitter:image" content="{image}" />
</svelte:head>
<div class={'leather min-h-full w-full flex flex-col items-center'}> <div class={'leather min-h-full w-full flex flex-col items-center'}>
<Navigation class='sticky top-0' /> <Navigation class='sticky top-0' />
<slot /> <slot />

5
src/routes/about/+page.svelte

@ -9,12 +9,13 @@
<div class='w-full flex justify-center'> <div class='w-full flex justify-center'>
<main class='main-leather flex flex-col space-y-6 max-w-2xl w-full my-6 px-4'> <main class='main-leather flex flex-col space-y-6 max-w-2xl w-full my-6 px-4'>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<Heading tag='h1' class='h-leather mb-2'>About</Heading> <Heading tag='h1' class='h-leather mb-2'>About the Library of Alexandria</Heading>
<Img src="/screenshots/old_books.jpg" alt="Alexandria icon" />
{#if isVersionKnown} {#if isVersionKnown}
<span class="text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded text-nowrap">Version: {appVersion}</span> <span class="text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded text-nowrap">Version: {appVersion}</span>
{/if} {/if}
</div> </div>
<P class="mb-3"> <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">curated publications</A> (in Asciidoc), wiki pages (Asciidoc), and will eventually also support long-form articles (Markdown). It is produced by the <A href="/publication?d=gitcitadel-project-documentation-gitcitadel-project-1-by-stella-v-1">GitCitadel project team</A>. Alexandria is a reader and writer for <A 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 (Markdown). It is produced by the <A href="/publication?d=gitcitadel-project-documentation-gitcitadel-project-1-by-stella-v-1">GitCitadel project team</A>.
</P> </P>

48
src/routes/publication/+page.svelte

@ -3,16 +3,60 @@
import { TextPlaceholder } from "flowbite-svelte"; import { TextPlaceholder } from "flowbite-svelte";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { onDestroy } from "svelte"; import { onDestroy } from "svelte";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { pharosInstance } from "$lib/parser";
import { page } from "$app/stores";
let { data }: { data: PageData } = $props(); // Extend the PageData type with the properties we need
interface ExtendedPageData extends PageData {
waitable: Promise<any>;
publicationType: string;
indexEvent: NDKEvent;
parser: any;
}
let { data } = $props<{ data: ExtendedPageData }>();
// Get publication metadata for OpenGraph tags
let title = $derived(data.indexEvent?.getMatchingTags('title')[0]?.[1] || data.parser?.getIndexTitle(data.parser?.getRootIndexId()) || 'Alexandria Publication');
let currentUrl = $page.url.href;
// Get image and summary from the event tags if available
// If image unavailable, use the Alexandria default pic.
let image = $derived(data.indexEvent?.getMatchingTags('image')[0]?.[1] || '/screenshots/old_books.jpg');
let summary = $derived(data.indexEvent?.getMatchingTags('summary')[0]?.[1] || 'Alexandria is a digital library, utilizing Nostr events for curated publications and wiki pages.');
onDestroy(() => data.parser.reset()); onDestroy(() => data.parser.reset());
</script> </script>
<svelte:head>
<!-- Basic meta tags -->
<title>{title}</title>
<meta name="description" content="{summary}" />
<!-- OpenGraph meta tags -->
<meta property="og:title" content="{title}" />
<meta property="og:description" content="{summary}" />
<meta property="og:url" content="{currentUrl}" />
<meta property="og:type" content="article" />
<meta property="og:site_name" content="Alexandria" />
<meta property="og:image" content="{image}" />
<!-- Twitter Card meta tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{title}" />
<meta name="twitter:description" content="{summary}" />
<meta name="twitter:image" content="{image}" />
</svelte:head>
<main> <main>
{#await data.waitable} {#await data.waitable}
<TextPlaceholder divClass='skeleton-leather w-full' size="xxl" /> <TextPlaceholder divClass='skeleton-leather w-full' size="xxl" />
{:then} {:then}
<Article rootId={data.parser.getRootIndexId()} publicationType={data.publicationType} /> <Article
rootId={data.parser.getRootIndexId()}
publicationType={data.publicationType}
indexEvent={data.indexEvent}
/>
{/await} {/await}
</main> </main>

5
src/routes/publication/+page.ts

@ -1,6 +1,6 @@
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import type { Load } from '@sveltejs/kit';
import type { NDKEvent } from '@nostr-dev-kit/ndk'; import type { NDKEvent } from '@nostr-dev-kit/ndk';
import type { PageLoad } from './$types';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { getActiveRelays } from '$lib/ndk.ts'; import { getActiveRelays } from '$lib/ndk.ts';
@ -82,7 +82,7 @@ async function fetchEventByDTag(ndk: any, dTag: string): Promise<NDKEvent> {
} }
} }
export const load: PageLoad = async ({ url, parent }: { url: URL; parent: () => Promise<any> }) => { export const load: Load = async ({ url, parent }: { url: URL; parent: () => Promise<any> }) => {
const id = url.searchParams.get('id'); const id = url.searchParams.get('id');
const dTag = url.searchParams.get('d'); const dTag = url.searchParams.get('d');
const { ndk, parser } = await parent(); const { ndk, parser } = await parent();
@ -102,5 +102,6 @@ export const load: PageLoad = async ({ url, parent }: { url: URL; parent: () =>
return { return {
waitable: fetchPromise, waitable: fetchPromise,
publicationType, publicationType,
indexEvent,
}; };
}; };

67
src/routes/visualize/+page.svelte

@ -79,44 +79,45 @@
</script> </script>
<div class="leather w-full p-4 relative"> <div class="leather w-full p-4 relative">
<h1 class="h-leather text-2xl font-bold mb-4">Publication Network</h1> <div class="flex items-center gap-4 mb-4">
<!-- Settings Toggle Button --> <h1 class="h-leather text-2xl font-bold">Publication Network</h1>
<!-- Settings Button - Using Flowbite Components --> <!-- Settings Button - Using Flowbite Components -->
{#if !loading && !error} {#if !loading && !error}
<Button <Button
class="btn-leather fixed right-4 top-24 z-40 rounded-lg min-w-[150px]" class="btn-leather z-10 rounded-lg min-w-[120px]"
size="sm" on:click={() => (showSettings = !showSettings)}
on:click={() => (showSettings = !showSettings)} >
> <CogSolid class="mr-2 h-5 w-5" />
<CogSolid class="mr-2 h-5 w-5" /> Settings
Settings </Button>
</Button> {/if}
</div>
{#if !loading && !error && showSettings}
<!-- Settings Panel --> <!-- Settings Panel -->
{#if showSettings} <div
<div class="absolute left-[220px] top-14 h-auto w-80 bg-white dark:bg-gray-800 p-4 shadow-lg z-10
class="fixed right-0 top-[140px] h-auto w-80 bg-white dark:bg-gray-800 p-4 shadow-lg z-30 overflow-y-auto max-h-[calc(100vh-96px)] rounded-lg border
overflow-y-auto max-h-[calc(100vh-96px)] rounded-l-lg border-l border-t border-b border-gray-200 dark:border-gray-700"
border-gray-200 dark:border-gray-700" transition:fly={{ duration: 300, y: -10, opacity: 1, easing: quintOut }}
transition:fly={{ duration: 300, x: 320, opacity: 1, easing: quintOut }} >
> <div class="card space-y-4">
<div class="card space-y-4"> <h2 class="text-xl font-bold mb-4 h-leather">
<h2 class="text-xl font-bold mb-4 h-leather"> Visualization Settings
Visualization Settings </h2>
</h2>
<div class="space-y-4"> <div class="space-y-4">
<span class="text-sm text-gray-600 dark:text-gray-400"> <span class="text-sm text-gray-600 dark:text-gray-400">
Showing {events.length} events from {$networkFetchLimit} headers Showing {events.length} events from {$networkFetchLimit} headers
</span> </span>
<EventLimitControl on:update={handleLimitUpdate} /> <EventLimitControl on:update={handleLimitUpdate} />
<EventRenderLevelLimit on:update={handleLimitUpdate} /> <EventRenderLevelLimit on:update={handleLimitUpdate} />
</div>
</div> </div>
</div> </div>
{/if} </div>
{/if} {/if}
{#if loading} {#if loading}
<div class="flex justify-center items-center h-64"> <div class="flex justify-center items-center h-64">
<div role="status"> <div role="status">
@ -155,6 +156,6 @@
</div> </div>
{:else} {:else}
<EventNetwork {events} /> <EventNetwork {events} />
<div class="mt-8 prose dark:prose-invert max-w-none" /> <div class="mt-8 prose dark:prose-invert max-w-none"></div>
{/if} {/if}
</div> </div>

BIN
static/favicon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
static/screenshots/old_books.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Loading…
Cancel
Save