clone of repo on github
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

415 lines
11 KiB

<script lang="ts">
import { pharosInstance, SiblingSearchDirection } from "$lib/parser";
import {
Button,
ButtonGroup,
CloseButton,
Input,
P,
Textarea,
Tooltip,
} from "flowbite-svelte";
import {
CaretDownSolid,
CaretUpSolid,
EditOutline,
} from "flowbite-svelte-icons";
import Self from "./Preview.svelte";
import {
contentParagraph,
sectionHeading,
} from "$lib/snippets/PublicationSnippets.svelte";
import BlogHeader from "$components/cards/BlogHeader.svelte";
import { getMatchingTags } from "$lib/utils/nostrUtils";
import { onMount } from "svelte";
// TODO: Fix move between parents.
let {
allowEditing,
depth = 0,
isSectionStart,
needsUpdate = $bindable<boolean>(),
oncursorcapture,
oncursorrelease,
parentId,
rootId,
index,
sectionClass,
publicationType,
onBlogUpdate,
} = $props<{
allowEditing?: boolean;
depth?: number;
isSectionStart?: boolean;
needsUpdate?: boolean;
oncursorcapture?: (e: MouseEvent) => void;
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);
let hasPreviousSibling: boolean = $state(false);
let hasNextSibling: boolean = $state(false);
let subtreeNeedsUpdate: boolean = $state(false);
let updateCount: number = $state(0);
let subtreeUpdateCount: number = $state(0);
let buttonsVisible: boolean = $derived(hasCursor && !childHasCursor);
$effect(() => {
if (needsUpdate) {
updateCount++;
needsUpdate = false;
title = $pharosInstance.getIndexTitle(rootId);
currentContent = $pharosInstance.getContent(rootId);
}
if (subtreeNeedsUpdate) {
subtreeUpdateCount++;
subtreeNeedsUpdate = false;
const prevChildCount = orderedChildren.length;
orderedChildren = $pharosInstance.getOrderedChildIds(rootId);
const newChildCount = orderedChildren.length;
// If the number of children has changed, a child has been added or removed, and a child may
// have been moved into a different subtree. Due to the `needsUpdate` binding in the
// component's recursion, setting `needsUpdate` to true will force the parent to rerender its
// subtree.
if (newChildCount !== prevChildCount) {
needsUpdate = true;
}
}
});
$effect(() => {
if (parentId && allowEditing) {
// Check for previous/next siblings on load
const previousSibling = $pharosInstance.getNearestSibling(
rootId,
depth - 1,
SiblingSearchDirection.Previous,
);
const nextSibling = $pharosInstance.getNearestSibling(
rootId,
depth - 1,
SiblingSearchDirection.Next,
);
// Hide arrows if no siblings exist
hasPreviousSibling = !!previousSibling[0];
hasNextSibling = !!nextSibling[0];
}
});
function getBlogEvent(index: number) {
return blogEntries[index][1];
}
function byline(rootId: string, index: number) {
console.log(rootId, index, blogEntries);
const event = blogEntries[index][1];
const author = event ? getMatchingTags(event, "author")[0][1] : "";
return author ?? "";
}
function hasCoverImage(rootId: string, index: number) {
console.log(rootId);
const event = blogEntries[index][1];
const image =
event && getMatchingTags(event, "image")[0]
? getMatchingTags(event, "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) {
oncursorcapture(e);
}
}
function handleMouseLeave(e: MouseEvent) {
hasCursor = false;
if (oncursorrelease) {
oncursorrelease(e);
}
}
function handleChildCursorCaptured(e: MouseEvent) {
childHasCursor = true;
if (oncursorcapture) {
oncursorcapture(e);
}
}
function handleChildCursorReleased(e: MouseEvent) {
childHasCursor = false;
}
function toggleEditing(id: string, shouldSave: boolean = true) {
const editing = isEditing;
if (editing && shouldSave) {
if (orderedChildren.length > 0) {
}
$pharosInstance.updateEventContent(id, currentContent);
}
isEditing = !editing;
}
function moveUp(rootId: string, parentId: string) {
// Get the previous sibling and its index
const [prevSiblingId, prevIndex] = $pharosInstance.getNearestSibling(
rootId,
depth - 1,
SiblingSearchDirection.Previous,
);
if (!prevSiblingId || prevIndex == null) {
return;
}
// Move the current event before the previous sibling.
$pharosInstance.moveEvent(rootId, prevSiblingId, false);
needsUpdate = true;
}
function moveDown(rootId: string, parentId: string) {
// Get the next sibling and its index
const [nextSiblingId, nextIndex] = $pharosInstance.getNearestSibling(
rootId,
depth - 1,
SiblingSearchDirection.Next,
);
if (!nextSiblingId || nextIndex == null) {
return;
}
// Move the current event after the next sibling
$pharosInstance.moveEvent(rootId, nextSiblingId, true);
needsUpdate = true;
}
</script>
{#snippet sectionHeading(title: string, depth: number)}
{@const headingLevel = Math.min(depth + 1, 6)}
{@const className = $pharosInstance.isFloatingTitle(rootId)
? "discrete"
: "h-leather"}
<svelte:element this={`h${headingLevel}`} class={className}>
{title}
</svelte:element>
{/snippet}
{#snippet coverImage(rootId: string, index: number, depth: number)}
{#if hasCoverImage(rootId, index)}
<div class="coverImage depth-{depth}">
<img src={hasCoverImage(rootId, index)} alt={title} />
</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 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}
</P>
{/if}
{/snippet}
<!-- This component is recursively structured. The base case is single block of content. -->
<section
id={rootId}
class={`note-leather flex space-x-2 justify-between text-wrap break-words ${sectionClass}`}
onmouseenter={handleMouseEnter}
onmouseleave={handleMouseLeave}
aria-label="Publication section"
>
<!-- Zettel base case -->
{#if orderedChildren.length === 0 || depth >= 4}
{#key updateCount}
{#if isEditing}
<form class="w-full">
<Textarea
class="textarea-leather w-full whitespace-normal"
bind:value={currentContent}
>
<div slot="footer" class="flex space-x-2 justify-end">
<Button
type="reset"
class="btn-leather min-w-fit"
size="sm"
outline
onclick={() => toggleEditing(rootId, false)}
>
Cancel
</Button>
<Button
type="submit"
class="btn-leather min-w-fit"
size="sm"
onclick={() => toggleEditing(rootId, true)}
>
Save
</Button>
</div>
</Textarea>
</form>
{:else}
{@render contentParagraph(currentContent, publicationType)}
{/if}
{/key}
{:else}
<div class="flex flex-col space-y-2 w-full">
{#if isEditing}
<ButtonGroup class="w-full">
<Input type="text" class="input-leather" size="lg" bind:value={title}>
<CloseButton
slot="right"
onclick={() => toggleEditing(rootId, false)}
/>
</Input>
<Button
class="btn-leather"
color="primary"
size="lg"
onclick={() => toggleEditing(rootId, true)}
>
Save
</Button>
</ButtonGroup>
{:else if !(publicationType === "blog" && depth === 1)}
{@render sectionHeading(title!, depth)}
{/if}
<!-- Recurse on child indices and zettels -->
{#if publicationType === "blog" && depth === 1}
<BlogHeader
event={getBlogEvent(index)}
{rootId}
onBlogUpdate={readBlog}
active={true}
/>
{:else}
{#key subtreeUpdateCount}
{#each orderedChildren as id, index}
<Self
rootId={id}
parentId={rootId}
{index}
{publicationType}
depth={depth + 1}
{allowEditing}
{sectionClass}
isSectionStart={index === 0}
bind:needsUpdate={subtreeNeedsUpdate}
oncursorcapture={handleChildCursorCaptured}
oncursorrelease={handleChildCursorReleased}
onBlogUpdate={propagateBlogUpdate}
/>
{/each}
{/key}
{/if}
</div>
{/if}
{#if allowEditing && depth > 0}
<div
class={`flex flex-col space-y-2 justify-start ${buttonsVisible ? "visible" : "invisible"}`}
>
{#if hasPreviousSibling && parentId}
<Button
class="btn-leather"
size="sm"
outline
onclick={() => moveUp(rootId, parentId)}
>
<CaretUpSolid />
</Button>
{/if}
{#if hasNextSibling && parentId}
<Button
class="btn-leather"
size="sm"
outline
onclick={() => moveDown(rootId, parentId)}
>
<CaretDownSolid />
</Button>
{/if}
<Button
class="btn-leather"
size="sm"
outline
onclick={() => toggleEditing(rootId)}
>
<EditOutline />
</Button>
<Tooltip class="tooltip-leather" type="auto" placement="top">
Edit
</Tooltip>
</div>
{/if}
</section>