mirror of
https://github.com/anotherhadi/blog.git
synced 2026-08-23 04:55:50 +02:00
edit sidebars
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
<script lang="ts">
|
||||
import { slide } from "svelte/transition";
|
||||
import { untrack } from "svelte";
|
||||
|
||||
interface Note {
|
||||
id: string;
|
||||
data: { title: string; tags: string[]; category?: string };
|
||||
body?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
notes: Note[];
|
||||
currentEntry?: Note;
|
||||
currentCategory?: string;
|
||||
categories: string[];
|
||||
}
|
||||
|
||||
const { notes, currentEntry, currentCategory, categories }: Props = $props();
|
||||
|
||||
let search = $state("");
|
||||
|
||||
function getCategory(n: Note): string {
|
||||
if (n.data.category) return n.data.category;
|
||||
const parts = n.id.split("/");
|
||||
return parts.length > 1 ? parts[0] : "General";
|
||||
}
|
||||
|
||||
function extractInlineHashtags(body: string): string[] {
|
||||
const re = /#(\w+)/g;
|
||||
const tags: string[] = [];
|
||||
let m;
|
||||
while ((m = re.exec(body)) !== null) tags.push(m[1].toLowerCase());
|
||||
return [...new Set(tags)];
|
||||
}
|
||||
|
||||
function matchesSearch(note: Note): boolean {
|
||||
const raw = search.toLowerCase().trim();
|
||||
if (!raw) return true;
|
||||
const isTag = raw.startsWith("#");
|
||||
const term = isTag ? raw.slice(1) : raw;
|
||||
const title = note.data.title.toLowerCase();
|
||||
const tags = [
|
||||
...note.data.tags,
|
||||
...extractInlineHashtags(note.body ?? ""),
|
||||
].map((t) => t.toLowerCase());
|
||||
return isTag
|
||||
? tags.some((t) => t.includes(term))
|
||||
: title.includes(term) || tags.join(",").includes(term);
|
||||
}
|
||||
|
||||
const activeCategory = $derived(
|
||||
currentCategory ?? (currentEntry ? getCategory(currentEntry) : null),
|
||||
);
|
||||
|
||||
let openCategories = $state<string[]>(
|
||||
untrack(() => categories.filter((c) => c === activeCategory)),
|
||||
);
|
||||
|
||||
function toggle(cat: string) {
|
||||
if (openCategories.includes(cat)) {
|
||||
openCategories = openCategories.filter((c) => c !== cat);
|
||||
} else {
|
||||
openCategories = [...openCategories, cat];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col flex-1 min-h-0">
|
||||
<div class="px-3 py-3 border-b border-base-300/40 shrink-0">
|
||||
<div
|
||||
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md bg-base-200/50 border border-base-300/40 focus-within:border-base-300/70 transition-colors"
|
||||
>
|
||||
<span class="text-base-content/30 font-mono text-xs shrink-0">›</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="search..."
|
||||
bind:value={search}
|
||||
class="flex-1 min-w-0 bg-transparent text-xs font-mono text-base-content/70 placeholder:text-base-content/25 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-2 py-2 space-y-px">
|
||||
{#each categories as cat}
|
||||
{@const catNotes = notes.filter(
|
||||
(n) => getCategory(n) === cat && matchesSearch(n),
|
||||
)}
|
||||
{#if catNotes.length > 0 || !search}
|
||||
{@const isFolder = notes.some((n) => n.id.includes("/") && getCategory(n) === cat)}
|
||||
<div>
|
||||
<div class="flex items-center w-full">
|
||||
<button
|
||||
onclick={() => toggle(cat)}
|
||||
class="flex items-center gap-1.5 px-2 py-1 rounded-md hover:bg-base-200/40 transition-colors duration-150 shrink-0"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 text-base-content/35 shrink-0 transition-transform duration-200"
|
||||
class:rotate-90={openCategories.includes(cat)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
<span class="text-primary/50 font-mono text-xs shrink-0">/</span>
|
||||
</button>
|
||||
{#if isFolder}
|
||||
<a
|
||||
href={`/notes/${cat}`}
|
||||
class="flex-1 min-w-0 px-1 py-1 rounded-md hover:bg-base-200/40 transition-colors duration-150 font-bold tracking-tight text-sm truncate text-base-content/80 hover:text-base-content"
|
||||
>
|
||||
{cat}
|
||||
</a>
|
||||
{:else}
|
||||
<span class="flex-1 min-w-0 px-1 py-1 font-bold tracking-tight text-sm truncate text-base-content/80">
|
||||
{cat}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if openCategories.includes(cat)}
|
||||
<ul
|
||||
class="ml-4 mt-0.5 pb-1 space-y-px"
|
||||
transition:slide={{ duration: 180 }}
|
||||
>
|
||||
{#each catNotes as note}
|
||||
<li class="tooltip tooltip-right w-full" data-tip={note.data.title}>
|
||||
<a
|
||||
href={`/notes/${note.id}`}
|
||||
class="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-mono truncate transition-colors duration-150
|
||||
{currentEntry && note.id === currentEntry.id
|
||||
? 'text-primary/90 bg-primary/10'
|
||||
: 'text-base-content/45 hover:text-base-content/80 hover:bg-base-200/40'}"
|
||||
>
|
||||
<span class="shrink-0 font-mono text-base-content/20">
|
||||
{currentEntry && note.id === currentEntry.id ? "▸" : "–"}
|
||||
</span>
|
||||
<span class="truncate">{note.data.title}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
Reference in New Issue
Block a user