Browse Source

Moving things around again

master
Nuša Pukšič 6 months ago committed by buttercat1791
parent
commit
42215020ca
  1. 6
      src/app.css
  2. 22
      src/app.html
  3. 112
      src/lib/a/cards/AProfilePreview.svelte
  4. 8
      src/lib/a/primitives/AAlert.svelte
  5. 6
      src/lib/a/reader/ATechBlock.svelte
  6. 3
      src/lib/a/reader/ATechToggle.svelte
  7. 2
      src/lib/components/CommentViewer.svelte
  8. 234
      src/lib/components/EventDetails.svelte
  9. 15
      src/lib/components/EventSearch.svelte
  10. 12
      src/lib/components/util/ArticleNav.svelte
  11. 10
      src/lib/components/util/CopyToClipboard.svelte
  12. 17
      src/lib/stores/techStore.ts
  13. 2
      src/routes/about/+page.svelte
  14. 2
      src/routes/contact/+page.svelte
  15. 92
      src/routes/events/+page.svelte
  16. 34
      src/routes/events/compose/+page.svelte
  17. 7
      src/routes/profile/+page.svelte
  18. 2
      src/routes/start/+page.svelte

6
src/app.css

@ -454,6 +454,10 @@
@apply fill-primary-600 dark:fill-primary-500; @apply fill-primary-600 dark:fill-primary-500;
} }
} }
[data-tech="off"] .tech-detail {
@apply !hidden;
}
} }
@layer components { @layer components {
@ -750,4 +754,4 @@
vertical-align: text-bottom; vertical-align: text-bottom;
font-weight: 500; font-weight: 500;
} }
} }

22
src/app.html

@ -1,10 +1,30 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-tech="off">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png?v=2" /> <link rel="icon" href="%sveltekit.assets%/favicon.png?v=2" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<!-- Apply saved theme ASAP to avoid flash -->
<script>
try {
const t = localStorage.getItem('theme');
if (t) document.documentElement.dataset.theme = t;
} catch (_) {
/* no-op */
}
</script>
<!-- Apply saved tech toggle ASAP; default is off -->
<script>
try {
const v = localStorage.getItem('alexandria/showTech');
document.documentElement.dataset.tech = v === 'true' ? 'on' : 'off';
} catch (_) {
/* no-op */
}
</script>
<!-- MathJax for math rendering --> <!-- MathJax for math rendering -->
<script> <script>
window.MathJax = { window.MathJax = {

112
src/lib/a/cards/AProfilePreview.svelte

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Card, Heading, P, Button, Modal, Avatar } from 'flowbite-svelte'; import { Card, Heading, P, Button, Modal, Avatar, Dropdown, DropdownItem } from 'flowbite-svelte';
import { ChevronDownOutline } from 'flowbite-svelte-icons';
import AAlert from '$lib/a/primitives/AAlert.svelte'; import AAlert from '$lib/a/primitives/AAlert.svelte';
import CopyToClipboard from '$lib/components/util/CopyToClipboard.svelte'; import CopyToClipboard from '$lib/components/util/CopyToClipboard.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@ -37,7 +38,7 @@
loading?: boolean; loading?: boolean;
error?: string | null; error?: string | null;
isOwn?: boolean; isOwn?: boolean;
event?: NDKEvent; event: NDKEvent;
communityStatusMap?: Record<string, boolean>; communityStatusMap?: Record<string, boolean>;
}>(); }>();
@ -149,72 +150,46 @@
<Card size="xl" class="main-leather p-0 overflow-hidden rounded-lg border border-primary-200 dark:border-primary-700"> <Card size="xl" class="main-leather p-0 overflow-hidden rounded-lg border border-primary-200 dark:border-primary-700">
{#if props.profile?.banner} {#if props.profile?.banner}
{#if props.event} <div class="w-full bg-primary-200 dark:bg-primary-800 relative">
<div class="w-full bg-primary-200 dark:bg-primary-800 relative"> <LazyImage src={props.profile.banner} alt="Profile banner" eventId={props.event.id} className="w-full h-60 object-cover" />
<LazyImage src={props.profile.banner} alt="Profile banner" eventId={props.event.id} className="w-full h-60 object-cover" /> </div>
</div> {:else}
{:else}
<div class="w-full h-60 bg-primary-200 dark:bg-primary-800 relative">
<img src={props.profile.banner} alt="Banner" class="w-full h-full object-cover" loading="lazy" onerror={hideOnError} />
</div>
{/if}
{:else if props.event}
<div class="w-full h-60" style={`background-color: ${generateDarkPastelColor(props.event.id)};`}></div> <div class="w-full h-60" style={`background-color: ${generateDarkPastelColor(props.event.id)};`}></div>
{/if} {/if}
<div class={`p-6 ${props.profile?.banner || props.event ? 'pt-6' : 'pt-6'} flex flex-col gap-4 relative`}> <div class={`p-6 flex flex-col gap-4 relative`}>
<Avatar size="xl" border src={props.profile?.picture} alt="Avatar" class="absolute w-fit top-[-56px]" />
<Avatar size="xl" border src={props.profile?.picture ?? null} alt="Avatar" class="absolute w-fit top-[-56px]" />
<div class="flex flex-col gap-3 mt-14">
<Heading tag="h1" class="h-leather mb-2">{displayName()}</Heading>
{#if props.user?.npub}
<CopyToClipboard displayText={shortNpub()} copyText={props.user.npub} />
{/if}
<div class="min-w-0 mt-14">
{#if props.event} {#if props.event}
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-2 min-w-0">
<div class="min-w-0 flex-1"> {#if props.profile?.nip05}
{@render userBadge( <span class="px-2 py-0.5 !mb-0 rounded bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 text-xs">{props.profile.nip05}</span>
toNpub(props.event.pubkey) as string, {/if}
props.profile?.displayName || props.profile?.display_name || props.profile?.name || props.event.pubkey,
ndk,
)}
</div>
{#if communityStatus === true} {#if communityStatus === true}
<div class="flex-shrink-0 w-4 h-4 bg-yellow-100 dark:bg-yellow-900 rounded-full flex items-center justify-center" title="Has posted to the community"> <div class="flex-shrink-0 w-4 h-4 bg-yellow-100 dark:bg-yellow-900 rounded-full flex items-center justify-center" title="Has posted to the community">
<svg class="w-3 h-3 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg> <svg class="w-3 h-3 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
</div> </div>
{:else if communityStatus === false}
<div class="flex-shrink-0 w-4 h-4"></div>
{/if} {/if}
{#if isInUserLists === true} {#if isInUserLists === true}
<div class="flex-shrink-0 w-4 h-4 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center" title="In your lists (follows, etc.)"> <div class="flex-shrink-0 w-4 h-4 bg-red-100 dark:bg-red-900 rounded-full flex items-center justify-center" title="In your lists (follows, etc.)">
<svg class="w-3 h-3 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg> <svg class="w-3 h-3 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
</div> </div>
{:else if isInUserLists === false}
<div class="flex-shrink-0 w-4 h-4"></div>
{/if} {/if}
</div> </div>
{:else}
<Heading tag="h1" class="h-leather mb-2">{displayName()}</Heading>
{/if} {/if}
<div class="flex flex-row flex-wrap items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mt-1">
{#if props.user?.npub}
<CopyToClipboard displayText={shortNpub()} copyText={props.user.npub} />
{/if}
{#if props.profile?.nip05}
<span class="px-2 py-0.5 !mb-0 rounded bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 text-xs">{props.profile.nip05}</span>
{/if}
{#if props.profile?.lud16}
<Button color="alternative" class="!mb-0 !py-0.5 !px-2 rounded" size="xs" onclick={() => (lnModalOpen = true)}> {props.profile.lud16}</Button>
{/if}
</div>
</div> </div>
{#if props.profile?.about} {#if props.profile?.about}
{#if props.event} <div class="prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100 break-words overflow-wrap-anywhere min-w-0">
<div class="prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100 break-words overflow-wrap-anywhere min-w-0"> {@render basicMarkup(props.profile.about, ndk)}
{@render basicMarkup(props.profile.about, ndk)} </div>
</div>
{:else}
<P class="whitespace-pre-wrap break-words leading-relaxed">{props.profile.about}</P>
{/if}
{/if} {/if}
<div class="flex flex-wrap gap-4 text-sm"> <div class="flex flex-wrap gap-4 text-sm">
@ -223,35 +198,22 @@
{/if} {/if}
</div> </div>
{#if props.event} <div class="flex flex-row flex-wrap justify-end gap-4 text-sm">
<div class="mt-4"> {#if props.profile?.lud16}
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Identifiers:</h4> <Button color="alternative" size="xs" onclick={() => (lnModalOpen = true)}> {props.profile.lud16}</Button>
<div class="flex flex-col gap-2 min-w-0"> {/if}
{#each getIdentifiers(props.event, props.profile) as identifier} <Button size="xs" color="alternative">Identifiers <ChevronDownOutline class="ms-2 h-6 w-6" /></Button>
<div class="flex items-center gap-2 min-w-0"> <Dropdown simple>
<span class="text-gray-600 dark:text-gray-400 flex-shrink-0">{identifier.label}:</span> {#each getIdentifiers(props.event, props.profile) as identifier}
<div class="flex-1 min-w-0 flex items-center gap-2"> <DropdownItem><CopyToClipboard displayText={identifier.label} copyText={identifier.value} /></DropdownItem>
{#if identifier.link} {/each}
<button class="font-mono text-sm text-primary-700 dark:text-primary-300 hover:text-primary-900 dark:hover:text-primary-100 break-all cursor-pointer bg-transparent border-none p-0 text-left" onclick={() => navigateToIdentifier(identifier.link!)}> </Dropdown>
{identifier.value}
</button> {#if props.isOwn}
{:else}
<span class="font-mono text-sm text-gray-900 dark:text-gray-100 break-all">{identifier.value}</span>
{/if}
<CopyToClipboard displayText="" copyText={identifier.value} />
</div>
</div>
{/each}
</div>
</div>
{/if}
{#if props.isOwn}
<div class="flex flex-row justify-end gap-4 text-sm">
<Button class="!mb-0" size="xs" onclick={() => goto('/profile/notifications')}>Notifications</Button> <Button class="!mb-0" size="xs" onclick={() => goto('/profile/notifications')}>Notifications</Button>
<Button class="!mb-0" size="xs" onclick={() => goto('/profile/my-notes')}>My notes</Button> <Button class="!mb-0" size="xs" onclick={() => goto('/profile/my-notes')}>My notes</Button>
</div> {/if}
{/if} </div>
{#if props.loading} {#if props.loading}
<AAlert color="primary">Loading profile…</AAlert> <AAlert color="primary">Loading profile…</AAlert>
@ -268,7 +230,7 @@
<div> <div>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
{@render userBadge( {@render userBadge(
props.event ? (toNpub(props.event.pubkey) as string) : (props.user?.npub || ''), props.user?.npub ?? toNpub(props.event.pubkey),
props.profile?.displayName || props.profile?.display_name || props.profile?.name || (props.event?.pubkey || ''), props.profile?.displayName || props.profile?.display_name || props.profile?.name || (props.event?.pubkey || ''),
ndk, ndk,
)} )}

8
src/lib/a/primitives/AAlert.svelte

@ -1,13 +1,19 @@
<script lang="ts"> <script lang="ts">
import { Alert } from "flowbite-svelte"; import { Alert } from "flowbite-svelte";
let { color, dismissable, children } = $props<{ let { color, dismissable, children, title } = $props<{
color?: string; color?: string;
dismissable?: boolean; dismissable?: boolean;
children?: any; children?: any;
title?: any;
}>(); }>();
</script> </script>
<Alert {color} {dismissable} class="alert-leather mb-4"> <Alert {color} {dismissable} class="alert-leather mb-4">
{#if title}
<div class="flex">
<span class="text-lg font-medium">{@render title()}</span>
</div>
{/if}
{@render children()} {@render children()}
</Alert> </Alert>

6
src/lib/a/reader/ATechBlock.svelte

@ -1,18 +1,16 @@
<script lang="ts"> <script lang="ts">
import { showTech } from '$lib/stores/techStore.ts'; import { showTech } from '$lib/stores/techStore.ts';
let revealed = false; let revealed = $state(false);
let { title = 'Technical details', className = '' , content} = $props(); let { title = 'Technical details', className = '' , content} = $props();
let hidden = $derived(!$showTech && !revealed); let hidden = $derived(!$showTech && !revealed);
</script> </script>
{#if hidden} {#if hidden}
<div class="rounded-md border border-dashed border-muted/40 bg-surface/60 px-3 py-2 flex items-center gap-3 {className}"> <div class="rounded-md border border-dashed border-muted/40 bg-surface/60 px-3 py-2 my-6 flex items-center gap-3 {className}">
<span class="text-xs opacity-70">{title} hidden</span> <span class="text-xs opacity-70">{title} hidden</span>
<button class="ml-auto text-sm underline hover:no-underline" onclick={() => revealed = true}>Reveal this block</button> <button class="ml-auto text-sm underline hover:no-underline" onclick={() => revealed = true}>Reveal this block</button>
</div> </div>
{:else} {:else}
<div class="rounded-md border border-muted/20 bg-surface p-3 {className}">
{@render content()} {@render content()}
</div>
{/if} {/if}

3
src/lib/a/reader/ATechToggle.svelte

@ -2,10 +2,9 @@
import { showTech } from '$lib/stores/techStore.ts'; import { showTech } from '$lib/stores/techStore.ts';
import { Toggle, P } from "flowbite-svelte"; import { Toggle, P } from "flowbite-svelte";
let label = 'Show technical details'; let label = 'Show technical details';
$: checked = $showTech;
</script> </script>
<div class="inline-flex items-center gap-2 select-none my-3"> <div class="inline-flex items-center gap-2 select-none my-3">
<Toggle {checked} ontoggle={() => $showTech = checked} aria-label={label} /> <Toggle bind:checked={$showTech} aria-label={label} />
<P class="text-sm">{label}</P> <P class="text-sm">{label}</P>
</div> </div>

2
src/lib/components/CommentViewer.svelte

@ -219,7 +219,7 @@
if (!isFetching) { if (!isFetching) {
fetchComments(); fetchComments();
} }
}, 2000); // Wait 2 seconds before retry }, 10000); // Wait 10 seconds before retry
} }
}); });

234
src/lib/components/EventDetails.svelte

@ -22,6 +22,9 @@
import { getNdkContext } from "$lib/ndk"; import { getNdkContext } from "$lib/ndk";
import type { UserProfile } from "$lib/models/user_profile"; import type { UserProfile } from "$lib/models/user_profile";
import { basicMarkup } from "$lib/snippets/MarkupSnippets.svelte"; import { basicMarkup } from "$lib/snippets/MarkupSnippets.svelte";
import ATechBlock from "$lib/a/reader/ATechBlock.svelte";
import { Accordion, AccordionItem, Heading } from "flowbite-svelte";
import RelayActions from "$components/RelayActions.svelte";
const { const {
event, event,
@ -302,6 +305,10 @@
return ids; return ids;
} }
function navigateToIdentifier(link: string) {
goto(link);
}
onMount(() => { onMount(() => {
function handleInternalLinkClick(event: MouseEvent) { function handleInternalLinkClick(event: MouseEvent) {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
@ -323,38 +330,38 @@
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100 break-words"> <h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100 break-words">
{@render basicMarkup(getEventTitle(event), ndk)} {@render basicMarkup(getEventTitle(event), ndk)}
</h2> </h2>
{/if}
<div class="flex items-center space-x-2 min-w-0"> <div class="flex items-center space-x-2 min-w-0">
{#if toNpub(event.pubkey)} {#if toNpub(event.pubkey)}
<span class="text-gray-600 dark:text-gray-400 min-w-0" <span class="text-gray-600 dark:text-gray-400 min-w-0"
>Author: {@render userBadge( >Author: {@render userBadge(
toNpub(event.pubkey) || '', toNpub(event.pubkey) || '',
profile?.display_name || undefined, profile?.display_name || undefined,
ndk, ndk,
)}</span )}</span
> >
{:else} {:else}
<span class="text-gray-600 dark:text-gray-400 min-w-0 break-words" <span class="text-gray-600 dark:text-gray-400 min-w-0 break-words"
>Author: {profile?.display_name || event.pubkey}</span >Author: {profile?.display_name || event.pubkey}</span
> >
{/if} {/if}
</div> </div>
<div class="flex items-center space-x-2 min-w-0"> <div class="flex items-center space-x-2 min-w-0">
<span class="text-gray-700 dark:text-gray-300 flex-shrink-0">Kind:</span> <span class="text-gray-700 dark:text-gray-300 flex-shrink-0">Kind:</span>
<span class="font-mono flex-shrink-0">{event.kind}</span> <span class="font-mono flex-shrink-0">{event.kind}</span>
<span class="text-gray-700 dark:text-gray-300 flex-shrink-0" <span class="text-gray-700 dark:text-gray-300 flex-shrink-0"
>({getEventTypeDisplay(event)})</span >({getEventTypeDisplay(event)})</span
> >
</div> </div>
<div class="flex flex-col space-y-1 min-w-0"> <div class="flex flex-col space-y-1 min-w-0">
<span class="text-gray-700 dark:text-gray-300">Summary:</span> <span class="text-gray-700 dark:text-gray-300">Summary:</span>
<div class="prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100 break-words overflow-wrap-anywhere min-w-0"> <div class="prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100 break-words overflow-wrap-anywhere min-w-0">
{@render basicMarkup(getEventSummary(event), ndk)} {@render basicMarkup(getEventSummary(event), ndk)}
</div>
</div> </div>
</div> {/if}
<!-- Containing Publications --> <!-- Containing Publications -->
<ContainingIndexes {event} /> <ContainingIndexes {event} />
@ -424,119 +431,80 @@
<AProfilePreview event={event} profile={profile} communityStatusMap={communityStatusMap} /> <AProfilePreview event={event} profile={profile} communityStatusMap={communityStatusMap} />
{/if} {/if}
<!-- Raw Event JSON --> <ATechBlock>
<details {#snippet content()}
class="relative w-full max-w-2xl md:max-w-full bg-primary-50 dark:bg-primary-900 rounded p-4 overflow-hidden" <Heading tag="h3" class="h-leather my-6">
> Technical details
<summary </Heading>
class="cursor-pointer font-semibold text-primary-700 dark:text-primary-300 mb-2"
> <Accordion flush class="w-full">
Show details <AccordionItem open={false} >
</summary> {#snippet header()}Identifiers{/snippet}
{#if event}
<!-- Identifiers Section --> <div class="flex flex-col gap-2">
<div class="mb-4 max-w-full overflow-hidden"> {#each getIdentifiers(event, profile) as identifier}
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Identifiers:</h4> <div class="grid grid-cols-[max-content_minmax(0,1fr)_max-content] items-start gap-2 min-w-0">
<div class="flex flex-col gap-2 min-w-0"> <span class="min-w-24 text-gray-600 dark:text-gray-400">{identifier.label}:</span>
{#each getIdentifiers(event, profile) as identifier} <div class="min-w-0">
<div class="flex items-center gap-2 min-w-0"> {#if identifier.link}
<span class="text-gray-600 dark:text-gray-400 flex-shrink-0">{identifier.label}:</span> <button class="font-mono text-sm text-primary-700 dark:text-primary-300 hover:text-primary-900 dark:hover:text-primary-100 break-all cursor-pointer bg-transparent border-none p-0 text-left"
<div class="flex-1 min-w-0 flex items-center gap-2"> onclick={() => navigateToIdentifier(identifier.link)}>
{#if identifier.link} {identifier.value}
<a </button>
href={identifier.link} {:else}
class="font-mono text-sm text-primary-700 dark:text-primary-300 hover:text-primary-900 dark:hover:text-primary-100 break-all cursor-pointer" <span class="font-mono text-sm text-gray-900 dark:text-gray-100 break-all">{identifier.value}</span>
title={identifier.value} {/if}
> </div>
{identifier.value.slice(0, 20)}...{identifier.value.slice(-8)} <div class="justify-self-end">
</a> <CopyToClipboard displayText="" copyText={identifier.value} />
{:else} </div>
<span class="font-mono text-sm text-gray-900 dark:text-gray-100 break-all" title={identifier.value}> </div>
{identifier.value.slice(0, 20)}...{identifier.value.slice(-8)} {/each}
</span> </div>
{/if} {/if}
</AccordionItem>
<!-- Event Tags Section -->
{#if event.tags && event.tags.length}
<AccordionItem open={false}>
{#snippet header()}
Tags
{/snippet}
<div class="flex flex-wrap gap-2 break-words min-w-0">
{#each event.tags as tag}
{@const tagInfo = getTagButtonInfo(tag)}
{#if tagInfo.text && tagInfo.gotoValue}
<button
onclick={() => handleTagGoto(tagInfo.gotoValue || "")}
class="text-primary-700 dark:text-primary-300 cursor-pointer bg-transparent border-none p-0 text-left hover:text-primary-900 dark:hover:text-primary-100 break-all max-w-full"
>
{tagInfo.text}
</button>
{/if}
{/each}
</div>
</AccordionItem>
{/if}
<AccordionItem open={false} contentClass="relative">
{#snippet header()}Event JSON{/snippet}
<div class="absolute top-5 right-0 z-10">
<CopyToClipboard <CopyToClipboard
displayText="" displayText=""
copyText={identifier.value} copyText={JSON.stringify(event.rawEvent(), null, 2)}
/> />
</div> </div>
</div> {#if event}
{/each} <pre class="p-4 wrap-break-word bg-highlight dark:bg-primary-900">
</div> <code class="text-wrap">{JSON.stringify(event.rawEvent(), null, 2)}</code>
</div> </pre>
{/if}
<!-- Event Tags Section --> </AccordionItem>
{#if event.tags && event.tags.length}
<div class="mb-4 max-w-full overflow-hidden">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Event Tags:</h4>
<div class="flex flex-wrap gap-2 break-words min-w-0">
{#each event.tags as tag}
{@const tagInfo = getTagButtonInfo(tag)}
{#if tagInfo.text && tagInfo.gotoValue}
<button
onclick={() => {
// Handle different types of gotoValue
if (
tagInfo.gotoValue!.startsWith("naddr") ||
tagInfo.gotoValue!.startsWith("nevent") ||
tagInfo.gotoValue!.startsWith("npub") ||
tagInfo.gotoValue!.startsWith("nprofile") ||
tagInfo.gotoValue!.startsWith("note")
) {
// For naddr, nevent, npub, nprofile, note - navigate directly
goto(`/events?id=${tagInfo.gotoValue!}`);
} else if (tagInfo.gotoValue!.startsWith("/")) {
// For relative URLs - navigate directly
goto(tagInfo.gotoValue!);
} else if (tagInfo.gotoValue!.startsWith("d:")) {
// For d-tag searches - navigate to d-tag search
const dTag = tagInfo.gotoValue!.substring(2);
goto(`/events?d=${encodeURIComponent(dTag)}`);
} else if (tagInfo.gotoValue!.startsWith("t:")) {
// For t-tag searches - navigate to t-tag search
const tTag = tagInfo.gotoValue!.substring(2);
goto(`/events?t=${encodeURIComponent(tTag)}`);
} else if (/^[0-9a-fA-F]{64}$/.test(tagInfo.gotoValue!)) {
// AI-NOTE: E-tag navigation may cause comment feed update issues
// When navigating to a new event via e-tag, the CommentViewer component
// may experience timing issues that result in:
// - Empty comment feeds even when comments exist
// - UI flashing between different thread views
// - Delayed comment loading
// This is likely due to race conditions between event prop changes
// and comment fetching in the CommentViewer component.
navigateToEvent(tagInfo.gotoValue!);
} else {
// For other cases, try direct navigation
goto(`/events?id=${tagInfo.gotoValue!}`);
}
}}
class="text-primary-700 dark:text-primary-300 cursor-pointer bg-transparent border-none p-0 text-left hover:text-primary-900 dark:hover:text-primary-100 break-all max-w-full"
>
{tagInfo.text}
</button>
{/if}
{/each}
</div>
</div>
{/if}
<!-- Raw Event JSON Section --> <AccordionItem open={true}>
<div class="mb-4 max-w-full overflow-hidden"> {#snippet header()}Relay Info{/snippet}
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Raw Event JSON:</h4> <RelayActions {event} />
<div class="relative min-w-0"> </AccordionItem>
<div class="absolute top-0 right-0 z-10"> </Accordion>
<CopyToClipboard {/snippet}
displayText="" </ATechBlock>
copyText={JSON.stringify(event.rawEvent(), null, 2)}
/>
</div>
<pre
class="overflow-x-auto text-xs bg-highlight dark:bg-primary-900 rounded p-4 mt-2 font-mono break-words whitespace-pre-wrap min-w-0"
style="line-height: 1.7; font-size: 1rem;">
{JSON.stringify(event.rawEvent(), null, 2)}
</pre>
</div>
</div>
</details>
</div> </div>

15
src/lib/components/EventSearch.svelte

@ -18,6 +18,7 @@
import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils"; import { getMatchingTags, toNpub } from "$lib/utils/nostrUtils";
import { isEventId } from "$lib/utils/nostr_identifiers"; import { isEventId } from "$lib/utils/nostr_identifiers";
import type { SearchType } from "$lib/models/search_type"; import type { SearchType } from "$lib/models/search_type";
import { AAlert } from "$lib/a";
// Props definition // Props definition
let { let {
@ -903,21 +904,15 @@
<!-- Error Display --> <!-- Error Display -->
{#if showError} {#if showError}
<div <AAlert color="red">
class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg"
role="alert"
>
{localError || error} {localError || error}
</div> </AAlert>
{/if} {/if}
<!-- Success Display --> <!-- Success Display -->
{#if showSuccess} {#if showSuccess}
<div <AAlert color="green">
class="p-4 mb-4 text-sm text-green-700 bg-green-100 rounded-lg"
role="alert"
>
{getResultMessage()} {getResultMessage()}
</div> </AAlert>
{/if} {/if}
</div> </div>

12
src/lib/components/util/ArticleNav.svelte

@ -6,7 +6,7 @@
GlobeOutline, GlobeOutline,
ChartOutline, ChartOutline,
} from "flowbite-svelte-icons"; } from "flowbite-svelte-icons";
import { Button } from "flowbite-svelte"; import { Button, P } from "flowbite-svelte";
import { publicationColumnVisibility } from "$lib/stores"; import { publicationColumnVisibility } from "$lib/stores";
import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
@ -152,7 +152,7 @@
</script> </script>
<nav <nav
class="Navbar navbar-leather flex fixed top-[100px] sm:top-[106px] w-full min-h-[70px] px-2 sm:px-4 py-2.5 z-10 transition-transform duration-300 {isVisible class="Navbar navbar-leather flex fixed top-[100px] sm:top-[92px] w-full min-h-[70px] px-2 sm:px-4 py-2.5 z-10 transition-transform duration-300 {isVisible
? 'translate-y-0' ? 'translate-y-0'
: '-translate-y-full'}" : '-translate-y-full'}"
> >
@ -191,14 +191,14 @@
{/if} {/if}
</div> </div>
<div class="flex flex-col flex-grow text justify-center items-center"> <div class="flex flex-col flex-grow text justify-center items-center">
<p class="max-w-[60vw] line-ellipsis"> <P class="max-w-[60vw] line-ellipsis">
<b class="text-nowrap">{title}</b> <b class="text-nowrap">{title}</b>
</p> </P>
<p> <P>
<span class="whitespace-nowrap" <span class="whitespace-nowrap"
>by {@render userBadge(pubkey, author, ndk)}</span >by {@render userBadge(pubkey, author, ndk)}</span
> >
</p> </P>
</div> </div>
<div class="flex justify-end items-center space-x-2 md:min-w-52 min-w-8"> <div class="flex justify-end items-center space-x-2 md:min-w-52 min-w-8">
{#if $publicationColumnVisibility.inner} {#if $publicationColumnVisibility.inner}

10
src/lib/components/util/CopyToClipboard.svelte

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Button } from "flowbite-svelte";
import { import {
ClipboardCheckOutline, ClipboardCheckOutline,
ClipboardCleanOutline, ClipboardCleanOutline,
@ -42,15 +43,12 @@
} }
</script> </script>
<button class="btn-leather w-full text-left" onclick={copyToClipboard}> <button class="btn-leather w-full text-left dark:text-primary-100 p-1 rounded-xs cursor-pointer" onclick={copyToClipboard}>
{#if copied} {#if copied}
<ClipboardCheckOutline class="inline mr-2" /> Copied! <ClipboardCheckOutline class="inline mr-2" /> Copied!
{:else} {:else}
{#if icon === ClipboardCleanOutline} {@const TheIcon = icon}
<ClipboardCleanOutline class="inline mr-2" /> <TheIcon class="inline { displayText !== '' ? 'mr-2' : ''}" />
{:else if icon === ClipboardCheckOutline}
<ClipboardCheckOutline class="inline mr-2" />
{/if}
{displayText} {displayText}
{/if} {/if}
</button> </button>

17
src/lib/stores/techStore.ts

@ -1,5 +1,16 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
const KEY='showTech'; const KEY = 'alexandria/showTech';
const initial = typeof localStorage!=='undefined' ? localStorage.getItem(KEY)==='true' : true;
// Default false unless explicitly set to 'true' in localStorage
const initial = typeof localStorage !== 'undefined'
? localStorage.getItem(KEY) === 'true'
: false;
export const showTech = writable<boolean>(initial); export const showTech = writable<boolean>(initial);
showTech.subscribe(v=>{ if(typeof document!=='undefined'){ document.documentElement.dataset.tech = v ? 'on' : 'off'; localStorage.setItem(KEY,String(v)); } });
showTech.subscribe(v => {
if (typeof document !== 'undefined') {
document.documentElement.dataset.tech = v ? 'on' : 'off';
localStorage.setItem(KEY, String(v));
}
});

2
src/routes/about/+page.svelte

@ -11,7 +11,7 @@
const ndk = getNdkContext(); const ndk = getNdkContext();
</script> </script>
<div class="w-full max-w-3xl flex flex-col self-center mb-3"> <div class="w-full max-w-3xl flex flex-col self-center mb-3 px-2">
<div class="flex flex-col justify-between items-center mb-4"> <div class="flex flex-col justify-between items-center mb-4">
<Heading tag="h1" class="h-leather mb-2" <Heading tag="h1" class="h-leather mb-2"
>About the Library of Alexandria</Heading >About the Library of Alexandria</Heading

2
src/routes/contact/+page.svelte

@ -261,7 +261,7 @@
}); });
</script> </script>
<div class="w-full max-w-3xl flex flex-col self-center"> <div class="w-full max-w-3xl flex flex-col self-center mb-3 px-2">
<Heading tag="h1" class="h-leather mb-2">Contact GitCitadel</Heading> <Heading tag="h1" class="h-leather mb-2">Contact GitCitadel</Heading>
<P class="my-3"> <P class="my-3">

92
src/routes/events/+page.svelte

@ -1,11 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Heading, P } from "flowbite-svelte"; import { Heading, P, List, Li } from "flowbite-svelte";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import type { NDKEvent } from "$lib/utils/nostrUtils"; import type { NDKEvent } from "$lib/utils/nostrUtils";
import EventSearch from "$lib/components/EventSearch.svelte"; import EventSearch from "$lib/components/EventSearch.svelte";
import EventDetails from "$lib/components/EventDetails.svelte"; import EventDetails from "$lib/components/EventDetails.svelte";
import RelayActions from "$lib/components/RelayActions.svelte";
import CommentBox from "$lib/components/CommentBox.svelte"; import CommentBox from "$lib/components/CommentBox.svelte";
import CommentViewer from "$lib/components/CommentViewer.svelte"; import CommentViewer from "$lib/components/CommentViewer.svelte";
import { userBadge } from "$lib/snippets/UserSnippets.svelte"; import { userBadge } from "$lib/snippets/UserSnippets.svelte";
@ -14,7 +13,6 @@
toNpub, toNpub,
getUserMetadata, getUserMetadata,
} from "$lib/utils/nostrUtils"; } from "$lib/utils/nostrUtils";
import EventInput from "$lib/components/EventInput.svelte";
import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte"; import CopyToClipboard from "$lib/components/util/CopyToClipboard.svelte";
import { neventEncode, naddrEncode } from "$lib/utils"; import { neventEncode, naddrEncode } from "$lib/utils";
import { activeInboxRelays, getNdkContext } from "$lib/ndk"; import { activeInboxRelays, getNdkContext } from "$lib/ndk";
@ -33,6 +31,7 @@
import type { SearchType } from "$lib/models/search_type"; import type { SearchType } from "$lib/models/search_type";
import { clearAllCaches } from "$lib/utils/cache_manager"; import { clearAllCaches } from "$lib/utils/cache_manager";
import { basicMarkup } from "$lib/snippets/MarkupSnippets.svelte"; import { basicMarkup } from "$lib/snippets/MarkupSnippets.svelte";
import { AAlert } from "$lib/a";
// AI-NOTE: Add cache clearing function for testing second-order search // AI-NOTE: Add cache clearing function for testing second-order search
// This can be called from browser console: window.clearCache() // This can be called from browser console: window.clearCache()
@ -506,27 +505,25 @@
<P class="mb-3"> <P class="mb-3">
Search and explore Nostr events across the network. Find events by: Search and explore Nostr events across the network. Find events by:
</P> </P>
<ul <List class="mb-3 list-disc">
class="mb-3 list-disc list-inside space-y-1 text-sm text-gray-700 dark:text-gray-300" <Li>
>
<li>
<strong>Event identifiers:</strong> nevent, note, naddr, npub, nprofile, <strong>Event identifiers:</strong> nevent, note, naddr, npub, nprofile,
pubkey, or event ID pubkey, or event ID
</li> </Li>
<li><strong>NIP-05 addresses:</strong> username@domain.com</li> <Li><strong>NIP-05 addresses:</strong> username@domain.com</Li>
<li> <Li>
<strong>Profile names:</strong> Search by display name or username (use <strong>Profile names:</strong> Search by display name or username (use
"n:" prefix for exact matches) "n:" prefix for exact matches)
</li> </Li>
<li> <Li>
<strong>D-tags:</strong> Find events with specific d-tags using "d:tag-name" <strong>D-tags:</strong> Find events with specific d-tags using "d:tag-name"
</li> </Li>
<li> <Li>
<strong>T-tags:</strong> Find events tagged with specific topics using <strong>T-tags:</strong> Find events tagged with specific topics using
"t:topic" "t:topic"
</li> </Li>
</ul> </List>
<P class="mb-3 text-sm text-gray-600 dark:text-gray-400"> <P class="mb-3 text-sm text-muted">
The page shows primary search results, second-order references The page shows primary search results, second-order references
(replies, quotes, mentions), and related tagged events. Click any (replies, quotes, mentions), and related tagged events. Click any
event to view details, comments, and relay information. event to view details, comments, and relay information.
@ -545,11 +542,9 @@
/> />
{#if secondOrderSearchMessage} {#if secondOrderSearchMessage}
<div <AAlert color="blue">
class="mt-4 p-4 text-sm text-blue-700 bg-blue-100 dark:bg-blue-900 dark:text-blue-200 rounded-lg"
>
{secondOrderSearchMessage} {secondOrderSearchMessage}
</div> </AAlert>
{/if} {/if}
{#if searchResults.length > 0} {#if searchResults.length > 0}
@ -1318,36 +1313,6 @@
</div> </div>
</div> </div>
{/if} {/if}
{#if !event && searchResults.length === 0 && secondOrderResults.length === 0 && tTagResults.length === 0 && !searchValue && !searchInProgress}
<div class="mt-8 w-full">
<Heading tag="h2" class="h-leather mb-4"
>Publish Nostr Event</Heading
>
<P class="mb-4">
Create and publish new Nostr events to the network. This form
supports various event kinds including:
</P>
<ul
class="mb-6 list-disc list-inside space-y-1 text-sm text-gray-700 dark:text-gray-300"
>
<li>
<strong>Kind 30040:</strong> Publication indexes that organize AsciiDoc
content into structured publications
</li>
<li>
<strong>Kind 30041:</strong> Individual section content for publications
</li>
<li>
<strong>Other kinds:</strong> Standard Nostr events with custom tags
and content
</li>
</ul>
<div class="w-full flex justify-center">
<EventInput />
</div>
</div>
{/if}
</div> </div>
</div> </div>
@ -1392,26 +1357,17 @@
<div class="min-w-0 overflow-hidden"> <div class="min-w-0 overflow-hidden">
<EventDetails {event} {profile} communityStatusMap={communityStatus} /> <EventDetails {event} {profile} communityStatusMap={communityStatus} />
</div> </div>
<div class="min-w-0 overflow-hidden">
<RelayActions {event} />
</div>
<div class="min-w-0 overflow-hidden"> <div class="flex flex-col space-y-6">
<CommentViewer {event} /> <CommentViewer {event} />
</div> {#if user?.signedIn}
{#if user?.signedIn}
<div class="mt-8 min-w-0 overflow-hidden">
<Heading tag="h3" class="h-leather mb-4 break-words"
>Add Comment</Heading
>
<CommentBox {event} {userRelayPreference} /> <CommentBox {event} {userRelayPreference} />
</div> {:else}
{:else} <AAlert color="blue">
<div class="mt-8 p-4 bg-gray-200 dark:bg-gray-700 rounded-lg min-w-0"> Please sign in to add comments.
<P>Please sign in to add comments.</P> </AAlert>
</div> {/if}
{/if} </div>
</div> </div>
{/if} {/if}
</div> </div>

34
src/routes/events/compose/+page.svelte

@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import { Heading, P } from "flowbite-svelte"; import { Heading, P, List, Li } from "flowbite-svelte";
import EventInput from "$components/EventInput.svelte"; import EventInput from "$components/EventInput.svelte";
import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk.ts"; import { activeInboxRelays, activeOutboxRelays } from "$lib/ndk.ts";
import { userStore } from "$lib/stores/userStore.ts"; import { userStore } from "$lib/stores/userStore.ts";
import { AAlert } from "$lib/a";
// AI-NOTE: 2025-01-24 - Reactive effect to log relay configuration when stores change - non-blocking approach // AI-NOTE: 2025-01-24 - Reactive effect to log relay configuration when stores change - non-blocking approach
$effect.pre(() => { $effect.pre(() => {
@ -27,18 +28,39 @@
<div class="main-leather flex flex-col space-y-6"> <div class="main-leather flex flex-col space-y-6">
<Heading tag="h1" class="h-leather mb-2">Compose Event</Heading> <Heading tag="h1" class="h-leather mb-2">Compose Event</Heading>
<P class="mb-3"> <P class="my-3">
Use this page to compose and publish various types of events to the Nostr network. Use this page to compose and publish various types of events to the Nostr network.
You can create notes, articles, and other event types depending on your needs. You can create notes, articles, and other event types depending on your needs.
</P> </P>
<P class="mb-4">
Create and publish new Nostr events to the network. This form
supports various event kinds including:
</P>
<List
class="mb-6 list-disc list-inside space-y-1"
>
<Li>
<strong>Kind 30040:</strong> Publication indexes that organize AsciiDoc
content into structured publications
</Li>
<Li>
<strong>Kind 30041:</strong> Individual section content for publications
</Li>
<Li>
<strong>Other kinds:</strong> Standard Nostr events with custom tags
and content
</Li>
</List>
{#if $userStore.signedIn} {#if $userStore.signedIn}
<EventInput /> <EventInput />
{:else} {:else}
<div class="p-6 bg-gray-200 dark:bg-gray-700 rounded-lg text-center"> <AAlert color="blue">
<Heading tag="h3" class="h-leather mb-4">Sign In Required</Heading> {#snippet title()}Sign In Required{/snippet}
<P>Please sign in to compose and publish events to the Nostr network.</P> Please sign in to compose and publish events to the Nostr network.
</div> </AAlert>
{/if} {/if}
</div> </div>
</div> </div>

7
src/routes/profile/+page.svelte

@ -6,7 +6,7 @@
import { getUserMetadata } from "$lib/utils/nostrUtils"; import { getUserMetadata } from "$lib/utils/nostrUtils";
import type { NDKEvent } from "$lib/utils/nostrUtils"; import type { NDKEvent } from "$lib/utils/nostrUtils";
import { getNdkContext } from "$lib/ndk.ts"; import { getNdkContext } from "$lib/ndk.ts";
import { Heading } from "flowbite-svelte"; import { Heading, P } from "flowbite-svelte";
import ATechToggle from "$lib/a/reader/ATechToggle.svelte"; import ATechToggle from "$lib/a/reader/ATechToggle.svelte";
// State // State
@ -82,8 +82,9 @@
{:else} {:else}
<div class="w-full flex justify-center"> <div class="w-full flex justify-center">
<div class="flex flex-col w-full max-w-5xl my-6 px-4 mx-auto gap-6"> <div class="flex flex-col w-full max-w-5xl my-6 px-4 mx-auto gap-6">
<AProfilePreview user={user} profile={profile} loading={loading} error={error} isOwn={!!user?.signedIn && (!profileEvent?.pubkey || profileEvent.pubkey === user.pubkey)} /> {#if profileEvent}
<AProfilePreview event={profileEvent} user={user} profile={profile} loading={loading} error={error} isOwn={!!user?.signedIn && (!profileEvent?.pubkey || profileEvent.pubkey === user.pubkey)} />
{/if}
<div class="mt-6"> <div class="mt-6">
<Heading tag="h3" class="h-leather mb-4"> <Heading tag="h3" class="h-leather mb-4">
Settings Settings

2
src/routes/start/+page.svelte

@ -7,7 +7,7 @@
const isVersionKnown = appVersion !== "development"; const isVersionKnown = appVersion !== "development";
</script> </script>
<div class="w-full max-w-2xl flex flex-col self-center"> <div class="w-full max-w-3xl flex flex-col self-center mb-3 px-2">
<Heading tag="h1" class="h-leather mb-2" <Heading tag="h1" class="h-leather mb-2"
>Getting Started with Alexandria</Heading >Getting Started with Alexandria</Heading
> >

Loading…
Cancel
Save