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 @@ @@ -55,12 +55,14 @@
/* Content */
main {
@apply max-w-full flex mb-2;
@apply max-w-full flex;
}
/* To scroll columns independently */
main.publication {
/* max-height: calc(100vh - 130px); */
main.publication.blog {
display: flex;
flex-direction: column;
max-height: calc(100vh - 76px);
}
main.main-leather,
@ -247,6 +249,17 @@ @@ -247,6 +249,17 @@
.link {
@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 {

63
src/lib/components/Publication.svelte

@ -9,12 +9,14 @@ @@ -9,12 +9,14 @@
TextPlaceholder,
Tooltip,
} from "flowbite-svelte";
import { CaretLeftOutline } from 'flowbite-svelte-icons';
import { getContext, onMount } from "svelte";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import PublicationSection from "./PublicationSection.svelte";
import type { PublicationTree } from "$lib/data_structures/publication_tree";
import Details from "$components/util/Details.svelte";
import { publicationColumnVisibility } from "$lib/stores";
import BlogHeader from "$components/blog/BlogHeader.svelte";
let { rootAddress, publicationType, indexEvent } = $props<{
rootAddress: string,
@ -83,30 +85,15 @@ @@ -83,30 +85,15 @@
}
$publicationColumnVisibility.inner = true;
currentBlog = rootId;
// set current blog values for publication render
console.log(currentBlog);
}
// #region ToC
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",
});
}
}
function backToMain() {
$publicationColumnVisibility.blog = true;
$publicationColumnVisibility.inner = false;
}
// #endregion
onMount(() => {
// Set up the intersection observer.
observer = new IntersectionObserver((entries) => {
@ -134,13 +121,25 @@ @@ -134,13 +121,25 @@
{/if}
{#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' }
{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} />
</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}
<PublicationSection
rootAddress={rootAddress}
@ -149,13 +148,27 @@ @@ -149,13 +148,27 @@
ref={(el) => setLastElementRef(el, i)}
/>
{/each}
{/if}
</div>
{/if}
{#if currentBlog !== null && $publicationColumnVisibility.inner }
{#key currentBlog }
<div class="flex flex-col space-y-4 max-w-3xl overflow-auto flex-grow-2">
<span>Todo...</span>
<div class="flex flex-col px-2 max-w-3xl overflow-auto flex-grow-2">
<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>
{/key}
{/if}

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

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

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

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

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

@ -0,0 +1,62 @@ @@ -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 @@ @@ -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 @@ @@ -1,55 +1,55 @@
@layer components {
/* AsciiDoc content */
.note-leather p a {
.publication-leather p a {
@apply underline hover:text-primary-500 dark:hover:text-primary-400;
}
.note-leather section p {
.publication-leather section p {
@apply w-full;
}
.note-leather section p table {
.publication-leather section p table {
@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;
}
.note-leather section p table td .content:has(> .imageblock) {
.publication-leather section p table td .content:has(> .imageblock) {
@apply flex flex-col items-center;
}
.note-leather .imageblock {
.publication-leather .imageblock {
@apply flex flex-col space-y-2;
}
.note-leather .imageblock .content {
.publication-leather .imageblock .content {
@apply flex justify-center;
}
.note-leather .imageblock .title {
.publication-leather .imageblock .title {
@apply text-center;
}
.note-leather .imageblock.left .content {
.publication-leather .imageblock.left .content {
@apply justify-start;
}
.note-leather .imageblock.left .title {
.publication-leather .imageblock.left .title {
@apply text-left;
}
.note-leather .imageblock.right .content {
.publication-leather .imageblock.right .content {
@apply justify-end;
}
.note-leather .imageblock.right .title {
.publication-leather .imageblock.right .title {
@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;
}
.note-leather .literalblock pre {
.publication-leather .literalblock pre {
@apply p-3 text-wrap break-words;
}
@ -58,7 +58,7 @@ @@ -58,7 +58,7 @@
}
/* lists */
.note-leather .ulist ul {
.publication-leather .ulist ul {
@apply space-y-1 list-disc list-inside;
}
@ -253,12 +253,8 @@ @@ -253,12 +253,8 @@
/** 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;
.blog .discreet .card-leather:not(:hover) {
@apply bg-primary-50 opacity-75 transition duration-500 ease-in-out ;
}
}
}

3
src/styles/scrollbar.css

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

Loading…
Cancel
Save