Browse Source

Blog contents with placeholder interactions

master
Nuša Pukšič 11 months ago
parent
commit
0595916644
  1. 19
      src/app.css
  2. 63
      src/lib/components/Publication.svelte
  3. 40
      src/lib/components/blog/BlogHeader.svelte
  4. 1
      src/lib/components/util/CardActions.svelte
  5. 62
      src/lib/components/util/Interactions.svelte
  6. 19
      src/lib/components/util/ZapOutline.svelte
  7. 38
      src/styles/publications.css
  8. 3
      src/styles/scrollbar.css

19
src/app.css

@ -55,12 +55,14 @@
/* Content */ /* Content */
main { main {
@apply max-w-full flex mb-2; @apply max-w-full flex;
} }
/* To scroll columns independently */ /* To scroll columns independently */
main.publication { main.publication.blog {
/* max-height: calc(100vh - 130px); */ display: flex;
flex-direction: column;
max-height: calc(100vh - 76px);
} }
main.main-leather, main.main-leather,
@ -247,6 +249,17 @@
.link { .link {
@apply underline cursor-pointer hover:text-primary-400 dark:hover:text-primary-500; @apply underline cursor-pointer hover:text-primary-400 dark:hover:text-primary-500;
} }
/* Card with transition */
.ArticleBox.grid .ArticleBoxImage {
@apply max-h-0;
transition: max-height 0.5s ease;
}
.ArticleBox.grid.active .ArticleBoxImage {
@apply max-h-72;
}
} }
@layer components { @layer components {

63
src/lib/components/Publication.svelte

@ -9,12 +9,14 @@
TextPlaceholder, TextPlaceholder,
Tooltip, Tooltip,
} from "flowbite-svelte"; } from "flowbite-svelte";
import { CaretLeftOutline } from 'flowbite-svelte-icons';
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import PublicationSection from "./PublicationSection.svelte"; import PublicationSection from "./PublicationSection.svelte";
import type { PublicationTree } from "$lib/data_structures/publication_tree"; import type { PublicationTree } from "$lib/data_structures/publication_tree";
import Details from "$components/util/Details.svelte"; import Details from "$components/util/Details.svelte";
import { publicationColumnVisibility } from "$lib/stores"; import { publicationColumnVisibility } from "$lib/stores";
import BlogHeader from "$components/blog/BlogHeader.svelte";
let { rootAddress, publicationType, indexEvent } = $props<{ let { rootAddress, publicationType, indexEvent } = $props<{
rootAddress: string, rootAddress: string,
@ -83,30 +85,15 @@
} }
$publicationColumnVisibility.inner = true; $publicationColumnVisibility.inner = true;
currentBlog = rootId; currentBlog = rootId;
// set current blog values for publication render
console.log(currentBlog);
} }
// #region ToC function backToMain() {
$publicationColumnVisibility.blog = true;
$publicationColumnVisibility.inner = false;
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;
window.scrollTo({
top: offsetPosition,
behavior: "auto",
});
}
}
} }
// #endregion
onMount(() => { onMount(() => {
// Set up the intersection observer. // Set up the intersection observer.
observer = new IntersectionObserver((entries) => { observer = new IntersectionObserver((entries) => {
@ -134,13 +121,25 @@
{/if} {/if}
{#if isDefaultVisible()} {#if isDefaultVisible()}
<div class="flex flex-col space-y-4 overflow-auto <div class="flex flex-col px-2 space-y-4 overflow-auto
{publicationType === 'blog' ? 'max-w-xl flex-grow-1' : 'max-w-2xl flex-grow-2' } {publicationType === 'blog' ? 'max-w-xl flex-grow-1' : 'max-w-2xl flex-grow-2' }
{currentBlog !== null ? 'discreet' : ''} {currentBlog !== null && $publicationColumnVisibility.inner ? 'discreet' : ''}
"> ">
<div class="card-leather bg-highlight dark:bg-primary-800 p-4 mx-2 mb-4 rounded-lg border"> <div class="card-leather bg-highlight dark:bg-primary-800 p-4 mb-4 rounded-lg border">
<Details event={indexEvent} /> <Details event={indexEvent} />
</div> </div>
{#if publicationType === 'blog'}
<!-- List blog excerpts -->
{#each leaves as leaf, i}
<BlogHeader
rootId={leaf.tagAddress()}
event={leaf}
onBlogUpdate={loadBlog}
active={!(currentBlog !== null && $publicationColumnVisibility.inner)}
/>
{/each}
{:else}
{#each leaves as leaf, i} {#each leaves as leaf, i}
<PublicationSection <PublicationSection
rootAddress={rootAddress} rootAddress={rootAddress}
@ -149,13 +148,27 @@
ref={(el) => setLastElementRef(el, i)} ref={(el) => setLastElementRef(el, i)}
/> />
{/each} {/each}
{/if}
</div> </div>
{/if} {/if}
{#if currentBlog !== null && $publicationColumnVisibility.inner } {#if currentBlog !== null && $publicationColumnVisibility.inner }
{#key currentBlog } {#key currentBlog }
<div class="flex flex-col space-y-4 max-w-3xl overflow-auto flex-grow-2"> <div class="flex flex-col px-2 max-w-3xl overflow-auto flex-grow-2">
<span>Todo...</span> <div class="flex flex-row bg-primary-0 fixed top-[145px] w-full">
<Button color="none" class="p-0 my-1" onclick={backToMain}><CaretLeftOutline /> Back</Button>
</div>
{#each leaves as leaf, i}
{#if leaf.tagAddress() === currentBlog}
<PublicationSection
rootAddress={rootAddress}
leaves={leaves}
address={leaf.tagAddress()}
ref={(el) => setLastElementRef(el, i)}
/>
{/if}
{/each}
</div> </div>
{/key} {/key}
{/if} {/if}

40
src/lib/components/blog/BlogHeader.svelte

@ -1,19 +1,18 @@
<script lang="ts"> <script lang="ts">
import type { NDKEvent } from '@nostr-dev-kit/ndk'; import type { NDKEvent } from '@nostr-dev-kit/ndk';
import { Card, Img } from "flowbite-svelte"; import { scale } from 'svelte/transition';
import { Button, Card, Img } from "flowbite-svelte";
import InlineProfile from "$components/util/InlineProfile.svelte"; import InlineProfile from "$components/util/InlineProfile.svelte";
import { HeartOutline } from 'flowbite-svelte-icons'; import Interactions from "$components/util/Interactions.svelte";
import { quintOut } from "svelte/easing";
const { rootId, event, onBlogUpdate } = $props<{ rootId: String, event: NDKEvent, onBlogUpdate?: any; }>(); const { rootId, event, onBlogUpdate, active = true } = $props<{ rootId: String, event: NDKEvent, onBlogUpdate?: any, active: boolean }>();
let title: string = $derived(event.getMatchingTags('title')[0]?.[1]); let title: string = $derived(event.getMatchingTags('title')[0]?.[1]);
let author: string = $derived(event.getMatchingTags('author')[0]?.[1] ?? 'unknown'); let author: string = $derived(event.getMatchingTags('author')[0]?.[1] ?? 'unknown');
let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null); let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null);
let authorPubkey: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null); let authorPubkey: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null);
let likeCount = 0;
function publishedAt() { function publishedAt() {
const date = event.created_at ? new Date(event.created_at * 1000) : ''; const date = event.created_at ? new Date(event.created_at * 1000) : '';
if (date !== '') { if (date !== '') {
@ -33,33 +32,32 @@
</script> </script>
{#if title != null} {#if title != null}
<Card class='ArticleBox card-leather w-xl flex flex-col'> <Card class="ArticleBox card-leather w-full grid max-w-xl {active ? 'active' : ''}">
<div class='flex flex-col space-y-2'> <div class='space-y-2'>
<div class="flex flex-row justify-between my-2"> <div class="flex flex-row justify-between my-2">
<InlineProfile pubkey={authorPubkey} title={author} /> <InlineProfile pubkey={authorPubkey} title={author} />
<span class='text-gray-500'>{publishedAt()}</span> <span class='text-gray-500'>{publishedAt()}</span>
</div> </div>
{#if image} {#if image && active}
<div class="flex col justify-center"> <div class="ArticleBoxImage flex col justify-center"
<Img src={image} class="rounded w-full h-full object-cover"/> in:scale={{ start: 0.8, duration: 500, delay: 100, easing: quintOut }}
>
<Img src={image} class="rounded w-full max-h-72 object-cover"/>
</div> </div>
{/if} {/if}
<div class='flex flex-col flex-grow space-y-4'> <div class='flex flex-col flex-grow space-y-4'>
<button onclick={() => showBlog()} class='text-left'> <button onclick={() => showBlog()} class='text-left'>
<h2 class='text-lg font-bold line-clamp-2' title="{title}">{title}</h2> <h2 class='text-lg font-bold line-clamp-2' title="{title}">{title}</h2>
</button> </button>
<button class="underline text-right" onclick={() => showBlog()} >Read all about it...</button> {#if active}
</div> <Button color="none" class="underline justify-end p-0" onclick={() => showBlog()} ><span class="">Read all about it...</span></Button>
<div class='flex flex-row bg-primary-50'> {/if}
<div class='InteractiveMenu flex flex-row'>
<div class='flex flex-row shrink-0'><HeartOutline /><span>{likeCount}</span></div>
<div class='flex flex-row shrink-0'><HeartOutline /><span>{likeCount}</span></div>
<div class='flex flex-row shrink-0'><HeartOutline /><span>{likeCount}</span></div>
<div class='flex flex-row shrink-0'><HeartOutline /><span>{likeCount}</span></div>
<div class='flex flex-row shrink-0'><HeartOutline /><span>{likeCount}</span></div>
</div> </div>
{#if active}
<div class='flex flex-row '>
<Interactions rootId={rootId} event={event} />
</div> </div>
{/if}
</div> </div>
</Card> </Card>
{/if} {/if}

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

@ -10,7 +10,6 @@
import { Button, Modal, Popover } from "flowbite-svelte"; import { Button, Modal, Popover } from "flowbite-svelte";
import { standardRelays } from "$lib/consts"; import { standardRelays } from "$lib/consts";
import { neventEncode, naddrEncode } from "$lib/utils"; import { neventEncode, naddrEncode } from "$lib/utils";
import InlineProfile from "$components/util/InlineProfile.svelte";
import Details from "./Details.svelte"; import Details from "./Details.svelte";
let { event } = $props(); let { event } = $props();

62
src/lib/components/util/Interactions.svelte

@ -0,0 +1,62 @@
<script lang="ts">
import { HeartOutline, FilePenOutline, AnnotationOutline } from 'flowbite-svelte-icons';
import ZapOutline from "$components/util/ZapOutline.svelte";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { onMount } from "svelte";
import { ndkInstance } from '$lib/ndk';
const { rootId, event } = $props<{ rootId: String, event: NDKEvent }>();
// Reactive arrays to hold incoming events
let likes: NDKEvent[] = [];
let zaps: NDKEvent[] = [];
let highlights: NDKEvent[] = [];
let comments: NDKEvent[] = [];
// Reactive counts derived from array lengths
// Derived counts from store values
const likeCount = $derived(likes.length);
const zapCount = $derived(zaps.length);
const highlightCount = $derived(highlights.length);
const commentCount = $derived(comments.length);
/**
* Subscribe to Nostr events of a given kind that reference our root event via e-tag.
* Push new events into the provided array if not already present.
* Returns the subscription for later cleanup.
*/
function subscribeCount(kind: number, targetArray: NDKEvent[]) {
const sub = $ndkInstance.subscribe({
kinds: [kind],
'#a': [rootId]
});
sub.on('event', (evt: NDKEvent) => {
// Only add if we haven't seen this event ID yet
if (!targetArray.find(e => e.id === evt.id)) {
targetArray.push(evt);
}
});
return sub;
}
let subs: any[] = [];
onMount(() => {
// Subscribe to each kind; store subs for cleanup
subs.push(subscribeCount(7, likes)); // likes (Reaction)
subs.push(subscribeCount(9735, zaps)); // zaps (Zap Receipts)
subs.push(subscribeCount(30023, highlights)); // highlights (custom kind)
subs.push(subscribeCount(1, comments)); // comments (Text Notes)
});
</script>
<div class='InteractiveMenu flex flex-row justify-around align-middle text-primary-600 dark:text-gray-500'>
<div class='flex flex-row shrink-0 min-w-11'><HeartOutline size="lg" /><span>{likeCount}</span></div>
<div class='flex flex-row shrink-0 min-w-11'><ZapOutline /><span>{zapCount}</span></div>
<div class='flex flex-row shrink-0 min-w-11'><FilePenOutline size="lg"/><span>{highlightCount}</span></div>
<div class='flex flex-row shrink-0 min-w-11'><AnnotationOutline size="lg"/><span>{commentCount}</span></div>
</div>

19
src/lib/components/util/ZapOutline.svelte

@ -0,0 +1,19 @@
<script>
export let size = 24; // default size
export let className = '';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class={className}
viewBox="0 0 24 24"
>
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
</svg>

38
src/styles/publications.css

@ -1,55 +1,55 @@
@layer components { @layer components {
/* AsciiDoc content */ /* AsciiDoc content */
.note-leather p a { .publication-leather p a {
@apply underline hover:text-primary-500 dark:hover:text-primary-400; @apply underline hover:text-primary-500 dark:hover:text-primary-400;
} }
.note-leather section p { .publication-leather section p {
@apply w-full; @apply w-full;
} }
.note-leather section p table { .publication-leather section p table {
@apply w-full table-fixed space-x-2 space-y-2; @apply w-full table-fixed space-x-2 space-y-2;
} }
.note-leather section p table td { .publication-leather section p table td {
@apply p-2; @apply p-2;
} }
.note-leather section p table td .content:has(> .imageblock) { .publication-leather section p table td .content:has(> .imageblock) {
@apply flex flex-col items-center; @apply flex flex-col items-center;
} }
.note-leather .imageblock { .publication-leather .imageblock {
@apply flex flex-col space-y-2; @apply flex flex-col space-y-2;
} }
.note-leather .imageblock .content { .publication-leather .imageblock .content {
@apply flex justify-center; @apply flex justify-center;
} }
.note-leather .imageblock .title { .publication-leather .imageblock .title {
@apply text-center; @apply text-center;
} }
.note-leather .imageblock.left .content { .publication-leather .imageblock.left .content {
@apply justify-start; @apply justify-start;
} }
.note-leather .imageblock.left .title { .publication-leather .imageblock.left .title {
@apply text-left; @apply text-left;
} }
.note-leather .imageblock.right .content { .publication-leather .imageblock.right .content {
@apply justify-end; @apply justify-end;
} }
.note-leather .imageblock.right .title { .publication-leather .imageblock.right .title {
@apply text-right; @apply text-right;
} }
.note-leather section p table td .literalblock { .publication-leather section p table td .literalblock {
@apply my-2 p-2 border rounded border-gray-400 dark:border-gray-600; @apply my-2 p-2 border rounded border-gray-400 dark:border-gray-600;
} }
.note-leather .literalblock pre { .publication-leather .literalblock pre {
@apply p-3 text-wrap break-words; @apply p-3 text-wrap break-words;
} }
@ -58,7 +58,7 @@
} }
/* lists */ /* lists */
.note-leather .ulist ul { .publication-leather .ulist ul {
@apply space-y-1 list-disc list-inside; @apply space-y-1 list-disc list-inside;
} }
@ -253,12 +253,8 @@
/** blog */ /** blog */
@screen lg { @screen lg {
@media (hover: hover) { @media (hover: hover) {
.blog .discreet:not(:hover) .coverImage img { .blog .discreet .card-leather:not(:hover) {
@apply filter grayscale sepia brightness-75 opacity-50 transition duration-500 ease-in-out saturate-200 hue-rotate-20; @apply bg-primary-50 opacity-75 transition duration-500 ease-in-out ;
}
.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;
} }
} }
} }

3
src/styles/scrollbar.css

@ -1,13 +1,12 @@
@layer components { @layer components {
/* Global scrollbar styles */ /* Global scrollbar styles */
* { * {
scrollbar-width: thin; /* Firefox */
scrollbar-color: rgba(87, 66, 41, 0.8) transparent; /* Transparent track, default scrollbar thumb */ scrollbar-color: rgba(87, 66, 41, 0.8) transparent; /* Transparent track, default scrollbar thumb */
} }
/* Webkit Browsers (Chrome, Safari, Edge) */ /* Webkit Browsers (Chrome, Safari, Edge) */
*::-webkit-scrollbar { *::-webkit-scrollbar {
width: 8px; /* Thin scrollbar */ width: 12px; /* Thin scrollbar */
} }
*::-webkit-scrollbar-track { *::-webkit-scrollbar-track {

Loading…
Cancel
Save