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

2
src/lib/components/Navigation.svelte

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

84
src/lib/components/Preview.svelte

@ -15,8 +15,10 @@ @@ -15,8 +15,10 @@
oncursorrelease,
parentId,
rootId,
index,
sectionClass,
publicationType,
onBlogUpdate
} = $props<{
allowEditing?: boolean;
depth?: number;
@ -26,14 +28,19 @@ @@ -26,14 +28,19 @@
oncursorrelease?: (e: MouseEvent) => void;
parentId?: string | null | undefined;
rootId: string;
index: number;
sectionClass?: string;
publicationType?: string;
onBlogUpdate?: any;
}>();
let currentContent: string = $state($pharosInstance.getContent(rootId));
let title: string | undefined = $state($pharosInstance.getIndexTitle(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 hasCursor: boolean = $state(false);
let childHasCursor: boolean = $state(false);
@ -85,6 +92,44 @@ @@ -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) {
hasCursor = true;
if (oncursorcapture) {
@ -177,11 +222,38 @@ @@ -177,11 +222,38 @@
{/if}
{/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)}
{#if publicationType === 'novel'}
<P class='whitespace-normal' firstupper={isSectionStart}>
{@html content}
</P>
{:else if publicationType === 'blog'}
<P class='whitespace-normal' firstupper={false}>
{@html content}
</P>
{:else}
<P class='whitespace-normal' firstupper={false}>
{@html content}
@ -239,15 +311,25 @@ @@ -239,15 +311,25 @@
Save
</Button>
</ButtonGroup>
{:else}
{#if publicationType === 'blog' && depth === 1}
{@render coverImage(rootId, index, depth)}
{@render sectionHeading(title!, depth)}
{@render blogMetadata(rootId, index)}
{:else}
{@render sectionHeading(title!, depth)}
{/if}
{/if}
<!-- Recurse on child indices and zettels -->
{#if publicationType === 'blog' && depth === 1}
{@render readMoreLink(rootId, index)}
{:else }
{#key subtreeUpdateCount}
{#each orderedChildren as id, index}
<Self
rootId={id}
parentId={rootId}
index={index}
publicationType={publicationType}
depth={depth + 1}
{allowEditing}
@ -256,9 +338,11 @@ @@ -256,9 +338,11 @@
bind:needsUpdate={subtreeNeedsUpdate}
oncursorcapture={handleChildCursorCaptured}
oncursorrelease={handleChildCursorReleased}
onBlogUpdate={propagateBlogUpdate}
/>
{/each}
{/key}
{/if}
</div>
{/if}
{#if allowEditing && depth > 0}

139
src/lib/components/Publication.svelte

@ -1,19 +1,9 @@ @@ -1,19 +1,9 @@
<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 { pharosInstance } from "$lib/parser";
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 }>();
@ -21,111 +11,50 @@ @@ -21,111 +11,50 @@
console.error("Root ID does not match parser root index ID");
}
const tocBreakpoint = 1140;
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() {
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;
let currentBlog: null|string = $state(null);
window.scrollTo({
top: offsetPosition,
behavior: "auto",
});
}
function isDefaultVisible() {
if (publicationType !== 'blog') {
return true;
} else {
return $publicationColumnVisibility.blog;
}
}
/**
* Hides the table of contents sidebar when the window shrinks below a certain size. This
* prevents the sidebar from occluding the article content.
*/
function setTocVisibilityOnResize() {
showToc = window.innerWidth >= tocBreakpoint;
showTocButton = window.innerWidth < tocBreakpoint;
function loadBlog(rootId: string) {
// depending on the size of the screen, also toggle blog list visibility
if (window.innerWidth < 1024) {
$publicationColumnVisibility.blog = false;
}
/**
* 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;
$publicationColumnVisibility.inner = true;
currentBlog = rootId;
}
}
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();
</script>
window.addEventListener("resize", setTocVisibilityOnResize);
window.addEventListener("click", hideTocOnClick);
{#if $publicationColumnVisibility.details}
<div class="flex flex-col space-y-4 max-w-2xl overflow-auto flex-shrink flex-grow-1">
<!-- <Details {event} />-->
</div>
{/if}
return () => {
window.removeEventListener("hashchange", scrollToElementWithOffset);
window.removeEventListener("resize", setTocVisibilityOnResize);
window.removeEventListener("click", hideTocOnClick);
};
});
</script>
{#if isDefaultVisible()}
<div class="flex flex-col space-y-4 overflow-auto flex-grow-1
{publicationType === 'blog' ? 'max-w-xl' : 'max-w-2xl' }
{currentBlog !== null ? 'discreet' : ''}
">
<Preview {rootId} {publicationType} index={0} onBlogUpdate={loadBlog} />
</div>
{/if}
{#if showTocButton && !showToc}
<Button
class="btn-leather fixed top-20 left-4 h-6 w-6"
outline={true}
on:click={(ev) => {
showToc = true;
ev.stopPropagation();
}}
>
<BookOutline />
</Button>
<Tooltip>Show Table of Contents</Tooltip>
{#if currentBlog !== null && $publicationColumnVisibility.inner }
{#key currentBlog }
<div class="flex flex-col space-y-4 max-w-3xl overflow-auto flex-grow-2">
<Preview rootId={currentBlog} {publicationType} index={0} />
</div>
{/key}
{/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>
:global(.sidebar-group-leather) {

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

@ -4,8 +4,8 @@ @@ -4,8 +4,8 @@
import { EyeOutline, BookOutline } from "flowbite-svelte-icons";
import { Button } from "flowbite-svelte";
import { onMount } from "svelte";
import { ndkInstance } from "$lib/ndk";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { publicationColumnVisibility } from "$lib/stores";
import InlineProfile from "$components/util/InlineProfile.svelte";
let {
rootId,
@ -15,28 +15,39 @@ @@ -15,28 +15,39 @@
publicationType: string
}>();
let index: null|NDKEvent = $state(null);
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>
<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="actions">
<Button class='btn-leather !w-auto' outline={true} >
<EyeOutline class="!fill-none inline mr-1" /> Details
</Button>
<!-- <Button class='btn-leather !w-auto' outline={true} onclick={() => toggleColumn('details')} >-->
<!-- <EyeOutline class="!fill-none inline mr-1" /> Details-->
<!-- </Button>-->
{#if publicationType === 'blog'}
<Button class='btn-leather !w-auto' outline={true} >
<BookOutline class="!fill-none inline mr-1" /> Table of Contents
<Button class='btn-leather !w-auto' outline={true} onclick={() => toggleColumn('blog')} >
<BookOutline class="!fill-none inline mr-1" /><span class="hidden sm:inline">Table of Contents</span>
</Button>
{:else}
<TocToggle rootId={rootId} />
{/if}
</div>
<div class="publisher">
<!-- <InlineProfile />-->
</div>
</div>
</nav>

10
src/lib/stores.ts

@ -6,3 +6,13 @@ export let idList = writable<string[]>([]); @@ -6,3 +6,13 @@ export let idList = writable<string[]>([]);
export let alexandriaKinds = readable<number[]>([30040, 30041]);
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 @@ @@ -229,4 +229,33 @@
.audioblock .content audio {
@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 = { @@ -79,6 +79,14 @@ const config = {
listStyleType: {
'upper-alpha': 'upper-alpha', // Uppercase letters
'lower-alpha': 'lower-alpha', // Lowercase letters
},
flexGrow: {
'1': '1',
'2': '2',
'3': '3',
},
hueRotate: {
20: '20deg',
}
},
},

Loading…
Cancel
Save