Browse Source

Switch columns and styles, additional navigation

master
Nuša Pukšič 11 months ago
parent
commit
3969a60e07
  1. 16
      src/app.css
  2. 2
      src/lib/components/Navigation.svelte
  3. 84
      src/lib/components/Preview.svelte
  4. 139
      src/lib/components/Publication.svelte
  5. 33
      src/lib/components/util/ArticleNav.svelte
  6. 10
      src/lib/stores.ts
  7. 29
      src/styles/publications.css
  8. 8
      tailwind.config.cjs

16
src/app.css

@ -55,7 +55,11 @@
/* Content */ /* Content */
main { main {
@apply max-w-full; @apply max-w-full flex;
}
main.blog {
max-height: calc(100vh - 130px);
} }
main.main-leather, main.main-leather,
@ -69,9 +73,9 @@
@apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300 p-2 rounded; @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300 p-2 rounded;
} }
div.note-leather:hover:not(:has(.note-leather:hover)), .edit div.note-leather:hover:not(:has(.note-leather:hover)),
p.note-leather:hover:not(:has(.note-leather:hover)), .edit p.note-leather:hover:not(:has(.note-leather:hover)),
section.note-leather:hover:not(:has(.note-leather:hover)) { section.edit.note-leather:hover:not(:has(.note-leather:hover)) {
@apply hover:bg-primary-100 dark:hover:bg-primary-800; @apply hover:bg-primary-100 dark:hover:bg-primary-800;
} }
@ -128,6 +132,10 @@
} }
/* Navbar */ /* Navbar */
nav.Navbar.navbar-main {
@apply z-30;
}
nav.navbar-leather { nav.navbar-leather {
@apply bg-primary-0 dark:bg-primary-1000 z-10; @apply bg-primary-0 dark:bg-primary-1000 z-10;
} }

2
src/lib/components/Navigation.svelte

@ -7,7 +7,7 @@
let leftMenuOpen = $state(false); let leftMenuOpen = $state(false);
</script> </script>
<Navbar class={`Navbar navbar-leather ${className}`}> <Navbar class={`Navbar navbar-leather navbar-main ${className}`}>
<div class='flex flex-grow justify-between'> <div class='flex flex-grow justify-between'>
<NavBrand href='/'> <NavBrand href='/'>
<h1>Alexandria</h1> <h1>Alexandria</h1>

84
src/lib/components/Preview.svelte

@ -15,8 +15,10 @@
oncursorrelease, oncursorrelease,
parentId, parentId,
rootId, rootId,
index,
sectionClass, sectionClass,
publicationType, publicationType,
onBlogUpdate
} = $props<{ } = $props<{
allowEditing?: boolean; allowEditing?: boolean;
depth?: number; depth?: number;
@ -26,14 +28,19 @@
oncursorrelease?: (e: MouseEvent) => void; oncursorrelease?: (e: MouseEvent) => void;
parentId?: string | null | undefined; parentId?: string | null | undefined;
rootId: string; rootId: string;
index: number;
sectionClass?: string; sectionClass?: string;
publicationType?: string; publicationType?: string;
onBlogUpdate?: any;
}>(); }>();
let currentContent: string = $state($pharosInstance.getContent(rootId)); let currentContent: string = $state($pharosInstance.getContent(rootId));
let title: string | undefined = $state($pharosInstance.getIndexTitle(rootId)); let title: string | undefined = $state($pharosInstance.getIndexTitle(rootId));
let orderedChildren: string[] = $state($pharosInstance.getOrderedChildIds(rootId)); let orderedChildren: string[] = $state($pharosInstance.getOrderedChildIds(rootId));
let blogEntries = $state(Array.from($pharosInstance.getBlogEntries()));
let metadata = $state($pharosInstance.getIndexMetadata());
let isEditing: boolean = $state(false); let isEditing: boolean = $state(false);
let hasCursor: boolean = $state(false); let hasCursor: boolean = $state(false);
let childHasCursor: boolean = $state(false); let childHasCursor: boolean = $state(false);
@ -85,6 +92,44 @@
} }
}); });
function byline(rootId: string, index: number) {
console.log(rootId, index, blogEntries);
const event = blogEntries[index][1];
const author = event ? event.getMatchingTags("author")[0][1] : '';
return author ?? "";
}
function hasCoverImage(rootId: string, index: number) {
console.log(rootId);
const event = blogEntries[index][1];
const image = event && event.getMatchingTags("image")[0] ? event.getMatchingTags("image")[0][1] : '';
return image ?? '';
}
function publishedAt(rootId: string, index: number) {
console.log(rootId, index);
console.log(blogEntries[index]);
const event = blogEntries[index][1];
const date = event.created_at ? new Date(event.created_at * 1000) : '';
if (date !== '') {
const formattedDate = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "2-digit",
}).format(date);
return formattedDate ?? "";
}
return '';
}
function readBlog(rootId:string) {
onBlogUpdate?.(rootId);
}
function propagateBlogUpdate(rootId:string) {
onBlogUpdate?.(rootId);
}
function handleMouseEnter(e: MouseEvent) { function handleMouseEnter(e: MouseEvent) {
hasCursor = true; hasCursor = true;
if (oncursorcapture) { if (oncursorcapture) {
@ -177,11 +222,38 @@
{/if} {/if}
{/snippet} {/snippet}
{#snippet coverImage(rootId: string, index: number, depth: number)}
{#if hasCoverImage(rootId, index)}
<div class="coverImage depth-{depth}">
<img src={hasCoverImage(rootId, index)} >
</div>
{/if}
{/snippet}
{#snippet blogMetadata(rootId: string, index: number)}
<p class='h-leather'>
by {byline(rootId, index)}
</p>
<p class='h-leather italic text-sm'>
{publishedAt(rootId, index)}
</p>
{/snippet}
{#snippet readMoreLink(rootId: string, index: number)}
<p class='h-leather'>
<button class="underline" onclick={() => readBlog(rootId)}>Read all about it...</button>
</p>
{/snippet}
{#snippet contentParagraph(content: string, publicationType: string)} {#snippet contentParagraph(content: string, publicationType: string)}
{#if publicationType === 'novel'} {#if publicationType === 'novel'}
<P class='whitespace-normal' firstupper={isSectionStart}> <P class='whitespace-normal' firstupper={isSectionStart}>
{@html content} {@html content}
</P> </P>
{:else if publicationType === 'blog'}
<P class='whitespace-normal' firstupper={false}>
{@html content}
</P>
{:else} {:else}
<P class='whitespace-normal' firstupper={false}> <P class='whitespace-normal' firstupper={false}>
{@html content} {@html content}
@ -239,15 +311,25 @@
Save Save
</Button> </Button>
</ButtonGroup> </ButtonGroup>
{:else}
{#if publicationType === 'blog' && depth === 1}
{@render coverImage(rootId, index, depth)}
{@render sectionHeading(title!, depth)}
{@render blogMetadata(rootId, index)}
{:else} {:else}
{@render sectionHeading(title!, depth)} {@render sectionHeading(title!, depth)}
{/if} {/if}
{/if}
<!-- Recurse on child indices and zettels --> <!-- Recurse on child indices and zettels -->
{#if publicationType === 'blog' && depth === 1}
{@render readMoreLink(rootId, index)}
{:else }
{#key subtreeUpdateCount} {#key subtreeUpdateCount}
{#each orderedChildren as id, index} {#each orderedChildren as id, index}
<Self <Self
rootId={id} rootId={id}
parentId={rootId} parentId={rootId}
index={index}
publicationType={publicationType} publicationType={publicationType}
depth={depth + 1} depth={depth + 1}
{allowEditing} {allowEditing}
@ -256,9 +338,11 @@
bind:needsUpdate={subtreeNeedsUpdate} bind:needsUpdate={subtreeNeedsUpdate}
oncursorcapture={handleChildCursorCaptured} oncursorcapture={handleChildCursorCaptured}
oncursorrelease={handleChildCursorReleased} oncursorrelease={handleChildCursorReleased}
onBlogUpdate={propagateBlogUpdate}
/> />
{/each} {/each}
{/key} {/key}
{/if}
</div> </div>
{/if} {/if}
{#if allowEditing && depth > 0} {#if allowEditing && depth > 0}

139
src/lib/components/Publication.svelte

@ -1,19 +1,9 @@
<script lang="ts"> <script lang="ts">
import {
Button,
Sidebar,
SidebarGroup,
SidebarItem,
SidebarWrapper,
Skeleton,
TextPlaceholder,
Tooltip,
} from "flowbite-svelte";
import { onMount } from "svelte";
import { BookOutline } from "flowbite-svelte-icons";
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 Details from "$components/util/Details.svelte";
import { publicationColumnVisibility } from "$lib/stores";
let { rootId, publicationType } = $props<{ rootId: string, publicationType: string }>(); let { rootId, publicationType } = $props<{ rootId: string, publicationType: string }>();
@ -21,111 +11,50 @@
console.error("Root ID does not match parser root index ID"); console.error("Root ID does not match parser root index ID");
} }
const tocBreakpoint = 1140;
let activeHash = $state(page.url.hash); let activeHash = $state(page.url.hash);
let showToc: boolean = $state(true);
let showTocButton: boolean = $state(false);
function normalizeHashPath(str: string): string {
return str
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^\w-]/g, "");
}
function scrollToElementWithOffset() { let currentBlog: null|string = $state(null);
const hash = window.location.hash;
if (hash) {
const targetElement = document.querySelector(hash);
if (targetElement) {
const headerOffset = 80;
const elementPosition = targetElement.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.scrollY - headerOffset;
window.scrollTo({ function isDefaultVisible() {
top: offsetPosition, if (publicationType !== 'blog') {
behavior: "auto", return true;
}); } else {
} return $publicationColumnVisibility.blog;
} }
} }
/** function loadBlog(rootId: string) {
* Hides the table of contents sidebar when the window shrinks below a certain size. This // depending on the size of the screen, also toggle blog list visibility
* prevents the sidebar from occluding the article content. if (window.innerWidth < 1024) {
*/ $publicationColumnVisibility.blog = false;
function setTocVisibilityOnResize() {
showToc = window.innerWidth >= tocBreakpoint;
showTocButton = window.innerWidth < tocBreakpoint;
} }
$publicationColumnVisibility.inner = true;
/** currentBlog = rootId;
* Hides the table of contents sidebar when the user clicks outside of it.
*/
function hideTocOnClick(ev: MouseEvent) {
const target = ev.target as HTMLElement;
if (target.closest(".sidebar-leather") || target.closest(".btn-leather")) {
return;
}
if (showToc) {
showToc = false;
} }
} </script>
onMount(() => {
// Always check whether the TOC sidebar should be visible.
setTocVisibilityOnResize();
window.addEventListener("hashchange", scrollToElementWithOffset);
// Also handle the case where the user lands on the page with a hash in the URL
scrollToElementWithOffset();
window.addEventListener("resize", setTocVisibilityOnResize); {#if $publicationColumnVisibility.details}
window.addEventListener("click", hideTocOnClick); <div class="flex flex-col space-y-4 max-w-2xl overflow-auto flex-shrink flex-grow-1">
<!-- <Details {event} />-->
</div>
{/if}
return () => { {#if isDefaultVisible()}
window.removeEventListener("hashchange", scrollToElementWithOffset); <div class="flex flex-col space-y-4 overflow-auto flex-grow-1
window.removeEventListener("resize", setTocVisibilityOnResize); {publicationType === 'blog' ? 'max-w-xl' : 'max-w-2xl' }
window.removeEventListener("click", hideTocOnClick); {currentBlog !== null ? 'discreet' : ''}
}; ">
}); <Preview {rootId} {publicationType} index={0} onBlogUpdate={loadBlog} />
</script> </div>
{/if}
{#if showTocButton && !showToc} {#if currentBlog !== null && $publicationColumnVisibility.inner }
<Button {#key currentBlog }
class="btn-leather fixed top-20 left-4 h-6 w-6" <div class="flex flex-col space-y-4 max-w-3xl overflow-auto flex-grow-2">
outline={true} <Preview rootId={currentBlog} {publicationType} index={0} />
on:click={(ev) => { </div>
showToc = true; {/key}
ev.stopPropagation();
}}
>
<BookOutline />
</Button>
<Tooltip>Show Table of Contents</Tooltip>
{/if} {/if}
<!-- TODO: Get TOC from parser. -->
<!-- {#if showToc}
<Sidebar class='sidebar-leather fixed top-20 left-0 px-4 w-60' {activeHash}>
<SidebarWrapper>
<SidebarGroup class='sidebar-group-leather overflow-y-scroll'>
{#each events as event}
<SidebarItem
class='sidebar-item-leather'
label={event.getMatchingTags('title')[0][1]}
href={`${$page.url.pathname}#${normalizeHashPath(event.getMatchingTags('title')[0][1])}`}
/>
{/each}
</SidebarGroup>
</SidebarWrapper>
</Sidebar>
{/if} -->
<div class="flex flex-col space-y-4 max-w-2xl">
<Preview {rootId} {publicationType} />
</div>
<style> <style>
:global(.sidebar-group-leather) { :global(.sidebar-group-leather) {

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

@ -4,8 +4,8 @@
import { EyeOutline, BookOutline } from "flowbite-svelte-icons"; import { EyeOutline, BookOutline } from "flowbite-svelte-icons";
import { Button } from "flowbite-svelte"; import { Button } from "flowbite-svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { ndkInstance } from "$lib/ndk"; import { publicationColumnVisibility } from "$lib/stores";
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import InlineProfile from "$components/util/InlineProfile.svelte";
let { let {
rootId, rootId,
@ -15,28 +15,39 @@
publicationType: string publicationType: string
}>(); }>();
let index: null|NDKEvent = $state(null);
onMount(async () => { onMount(async () => {
index = await $ndkInstance.fetchEvent({ ids: [rootId] }); console.log($pharosInstance.getIndexMetadata());
});
// Function to toggle column visibility
function toggleColumn(column: 'details'|'blog') {
publicationColumnVisibility.update(store => {
store[column] = !store[column]; // Toggle true/false
if (window.innerWidth < 1140) {
$publicationColumnVisibility.inner = false;
}
return { ...store }; // Ensure reactivity
}); });
}
let metadata = $state($pharosInstance.getIndexMetadata());
</script> </script>
<nav class="Navbar navbar-leather sticky top-[76px] w-full px-2 sm:px-4 py-2.5 z-10"> <nav class="Navbar navbar-leather sticky top-[76px] w-full px-2 sm:px-4 py-2.5 z-10">
<div class="mx-auto flex flex-wrap justify-between items-center container"> <div class="mx-auto flex flex-wrap justify-between items-center container">
<div class="actions"> <div class="actions">
<Button class='btn-leather !w-auto' outline={true} > <!-- <Button class='btn-leather !w-auto' outline={true} onclick={() => toggleColumn('details')} >-->
<EyeOutline class="!fill-none inline mr-1" /> Details <!-- <EyeOutline class="!fill-none inline mr-1" /> Details-->
</Button> <!-- </Button>-->
{#if publicationType === 'blog'} {#if publicationType === 'blog'}
<Button class='btn-leather !w-auto' outline={true} > <Button class='btn-leather !w-auto' outline={true} onclick={() => toggleColumn('blog')} >
<BookOutline class="!fill-none inline mr-1" /> Table of Contents <BookOutline class="!fill-none inline mr-1" /><span class="hidden sm:inline">Table of Contents</span>
</Button> </Button>
{:else} {:else}
<TocToggle rootId={rootId} /> <TocToggle rootId={rootId} />
{/if} {/if}
</div> </div>
<div class="publisher">
<!-- <InlineProfile />-->
</div>
</div> </div>
</nav> </nav>

10
src/lib/stores.ts

@ -6,3 +6,13 @@ export let idList = writable<string[]>([]);
export let alexandriaKinds = readable<number[]>([30040, 30041]); export let alexandriaKinds = readable<number[]>([30040, 30041]);
export let feedType = writable<FeedType>(FeedType.StandardRelays); export let feedType = writable<FeedType>(FeedType.StandardRelays);
export const publicationColumnVisibility = writable({
details: false,
toc: false,
blog: true,
main: true,
inner: true,
social: false,
editing: false
});

29
src/styles/publications.css

@ -229,4 +229,33 @@
.audioblock .content audio { .audioblock .content audio {
@apply w-full; @apply w-full;
} }
.coverImage {
@apply max-h-[230px] overflow-hidden;
}
.coverImage.depth-0 {
@apply max-h-[460px] overflow-hidden;
}
.coverImage img {
@apply object-contain w-full;
}
.coverImage.depth-0 img {
@apply m-auto w-auto;
}
/** blog */
@screen lg {
@media (hover: hover) {
.blog .discreet:not(:hover) .coverImage img {
@apply filter grayscale sepia brightness-75 opacity-50 transition duration-500 ease-in-out saturate-200 hue-rotate-20;
}
.blog .discreet:not(:hover) .h-leather {
@apply filter grayscale sepia brightness-75 opacity-50 transition duration-500 ease-in-out saturate-200 hue-rotate-20;
}
}
}
} }

8
tailwind.config.cjs

@ -79,6 +79,14 @@ const config = {
listStyleType: { listStyleType: {
'upper-alpha': 'upper-alpha', // Uppercase letters 'upper-alpha': 'upper-alpha', // Uppercase letters
'lower-alpha': 'lower-alpha', // Lowercase letters 'lower-alpha': 'lower-alpha', // Lowercase letters
},
flexGrow: {
'1': '1',
'2': '2',
'3': '3',
},
hueRotate: {
20: '20deg',
} }
}, },
}, },

Loading…
Cancel
Save