mirror of
https://github.com/anotherhadi/blog.git
synced 2026-05-20 05:32:32 +02:00
change notes sidebars behavior
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,7 @@ const username = "anotherhadi"
|
|||||||
const bio = "Infosec engineer."
|
const bio = "Infosec engineer."
|
||||||
|
|
||||||
---
|
---
|
||||||
<div class="flex gap-3 justify-start">
|
<div class="flex flex-wrap gap-3 justify-start">
|
||||||
<div
|
<div
|
||||||
class="ring-base-300 ring-offset-base-100 rounded-full ring-2 ring-offset-2 flex justify-center items-center"
|
class="ring-base-300 ring-offset-base-100 rounded-full ring-2 ring-offset-2 flex justify-center items-center"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function isActive(href: string) {
|
|||||||
---
|
---
|
||||||
|
|
||||||
<header
|
<header
|
||||||
class="fixed top-0 left-0 right-0 z-50 h-12 flex items-center px-5"
|
class="fixed top-0 left-0 right-0 z-[60] h-12 flex items-center px-5"
|
||||||
style="background: oklch(0% 0 0 / 0.85); backdrop-filter: blur(12px); border-bottom: 1px solid oklch(22% 0 0);"
|
style="background: oklch(0% 0 0 / 0.85); backdrop-filter: blur(12px); border-bottom: 1px solid oklch(22% 0 0);"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full max-w-screen-xl mx-auto">
|
<div class="flex items-center justify-between w-full max-w-screen-xl mx-auto">
|
||||||
@@ -67,7 +67,7 @@ function isActive(href: string) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
id="mobile-menu"
|
id="mobile-menu"
|
||||||
class="hidden fixed inset-x-0 top-12 z-40 md:hidden border-b border-base-300/60 py-2"
|
class="hidden fixed inset-x-0 top-12 z-[59] md:hidden border-b border-base-300/60 py-2"
|
||||||
style="background: oklch(2% 0 0 / 0.97); backdrop-filter: blur(12px);"
|
style="background: oklch(2% 0 0 / 0.97); backdrop-filter: blur(12px);"
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const notes = defineCollection({
|
|||||||
schema: z.object({
|
schema: z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
category: z.string(),
|
category: z.string().optional(),
|
||||||
tags: z.array(z.string()).default([]),
|
tags: z.array(z.string()).default([]),
|
||||||
publishDate: z.coerce.date(),
|
publishDate: z.coerce.date(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { getCollection, render } from "astro:content";
|
import { getCollection, render } from "astro:content";
|
||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import { Shield, ChevronLeft, List, PanelRight } from "@lucide/astro";
|
import { Shield, ChevronLeft, List, PanelRight } from "@lucide/astro";
|
||||||
|
import Author from "../../components/Author.astro";
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const notes = await getCollection("notes");
|
const notes = await getCollection("notes");
|
||||||
@@ -16,7 +17,13 @@ const { Content } = await render(entry);
|
|||||||
|
|
||||||
const allNotes = await getCollection("notes");
|
const allNotes = await getCollection("notes");
|
||||||
const sortedNotes = allNotes.sort((a, b) => a.data.title.localeCompare(b.data.title));
|
const sortedNotes = allNotes.sort((a, b) => a.data.title.localeCompare(b.data.title));
|
||||||
const categories = [...new Set(allNotes.map((n) => n.data.category))].sort();
|
function getCategory(n: { id: string; data: { category?: string } }): string {
|
||||||
|
if (n.data.category) return n.data.category;
|
||||||
|
const parts = n.id.split("/");
|
||||||
|
return parts.length > 1 ? parts[0] : "General";
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = [...new Set(allNotes.map(getCategory))].sort();
|
||||||
|
|
||||||
function formatDate(date: Date) {
|
function formatDate(date: Date) {
|
||||||
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
||||||
@@ -50,6 +57,14 @@ const graphEdges = [
|
|||||||
...backlinks.map((n) => ({ from: n.id, to: entry.id })),
|
...backlinks.map((n) => ({ from: n.id, to: entry.id })),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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 slugify(text: string) {
|
function slugify(text: string) {
|
||||||
return text.toLowerCase().replace(/`[^`]*`/g, "").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-");
|
return text.toLowerCase().replace(/`[^`]*`/g, "").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-");
|
||||||
}
|
}
|
||||||
@@ -62,12 +77,21 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
|||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Both sidebars sit below the navbar when in drawer-open mode */
|
||||||
|
.drawer.lg\:drawer-open > .drawer-side,
|
||||||
|
.drawer.xl\:drawer-open > .drawer-side {
|
||||||
|
top: 3rem;
|
||||||
|
height: calc(100vh - 3rem);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
title={`${entry.data.title} — Security Notes`}
|
title={`${entry.data.title} — Security Notes`}
|
||||||
description={entry.data.description}
|
description={entry.data.description}
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="drawer drawer-end min-h-[calc(100vh-3rem)]">
|
<div class="drawer drawer-end xl:drawer-open min-h-[calc(100vh-3rem)]">
|
||||||
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
|
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
|
|
||||||
<div class="drawer-content flex min-h-[calc(100vh-3rem)]">
|
<div class="drawer-content flex min-h-[calc(100vh-3rem)]">
|
||||||
@@ -103,10 +127,10 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<header class="mb-10">
|
<header class="mb-8">
|
||||||
<div class="flex items-center gap-3 mb-5">
|
<div class="flex items-center gap-3 mb-5">
|
||||||
<span class="text-xl font-bold tracking-tight">
|
<span class="text-xl font-bold tracking-tight">
|
||||||
<span class="text-primary/50 font-mono mr-0.5">/</span>{entry.data.category}
|
<span class="text-primary/50 font-mono mr-0.5">/</span>{getCategory(entry)}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-base-content/20 text-xs">·</span>
|
<span class="text-base-content/20 text-xs">·</span>
|
||||||
<time datetime={entry.data.publishDate.toISOString()} class="text-xs text-base-content/35">
|
<time datetime={entry.data.publishDate.toISOString()} class="text-xs text-base-content/35">
|
||||||
@@ -116,7 +140,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
|||||||
<h1 class="text-4xl sm:text-5xl font-bold tracking-tight mb-3">{entry.data.title}</h1>
|
<h1 class="text-4xl sm:text-5xl font-bold tracking-tight mb-3">{entry.data.title}</h1>
|
||||||
<p class="text-base-content/50 mb-4">{entry.data.description}</p>
|
<p class="text-base-content/50 mb-4">{entry.data.description}</p>
|
||||||
{entry.data.tags.length > 0 && (
|
{entry.data.tags.length > 0 && (
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1 mb-4">
|
||||||
{entry.data.tags.map((tag) => (
|
{entry.data.tags.map((tag) => (
|
||||||
<a href={`/notes?tag=${tag}`}
|
<a href={`/notes?tag=${tag}`}
|
||||||
class="font-mono text-[10px] px-1.5 py-0.5 border border-base-300/40 text-base-content/25 hover:text-primary/70 hover:border-primary/40 transition-colors">
|
class="font-mono text-[10px] px-1.5 py-0.5 border border-base-300/40 text-base-content/25 hover:text-primary/70 hover:border-primary/40 transition-colors">
|
||||||
@@ -181,7 +205,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
|||||||
<div class="drawer-side z-50">
|
<div class="drawer-side z-50">
|
||||||
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||||
<aside
|
<aside
|
||||||
class="w-56 min-h-full flex flex-col border-r border-base-300/60"
|
class="w-56 flex flex-col border-r border-base-300/60 h-full"
|
||||||
style="background: oklch(4% 0 0);"
|
style="background: oklch(4% 0 0);"
|
||||||
>
|
>
|
||||||
<div class="px-4 py-4 border-b border-base-300/40">
|
<div class="px-4 py-4 border-b border-base-300/40">
|
||||||
@@ -208,7 +232,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="ml-3 space-y-0.5 border-l border-base-300/30 pl-2">
|
<ul class="ml-3 space-y-0.5 border-l border-base-300/30 pl-2">
|
||||||
{sortedNotes.filter((n) => n.data.category === cat).map((n) => (
|
{sortedNotes.filter((n) => getCategory(n) === cat).map((n) => (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={`/notes/${n.id}`}
|
href={`/notes/${n.id}`}
|
||||||
@@ -219,7 +243,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
|||||||
: "text-base-content/45 hover:text-base-content/80 hover:bg-base-200/30",
|
: "text-base-content/45 hover:text-base-content/80 hover:bg-base-200/30",
|
||||||
]}
|
]}
|
||||||
data-title={n.data.title.toLowerCase()}
|
data-title={n.data.title.toLowerCase()}
|
||||||
data-tags={n.data.tags.join(",")}
|
data-tags={[...n.data.tags, ...extractInlineHashtags(n.body ?? "")].join(",")}
|
||||||
>
|
>
|
||||||
{n.id === entry.id ? "▶ " : ""}{n.data.title}
|
{n.id === entry.id ? "▶ " : ""}{n.data.title}
|
||||||
</a>
|
</a>
|
||||||
@@ -239,7 +263,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
|||||||
<label for="graph-drawer" aria-label="close sidebar" class="drawer-overlay xl:hidden"></label>
|
<label for="graph-drawer" aria-label="close sidebar" class="drawer-overlay xl:hidden"></label>
|
||||||
<aside
|
<aside
|
||||||
id="right-sidebar"
|
id="right-sidebar"
|
||||||
class="w-52 min-h-full flex flex-col border-l border-base-300/60"
|
class="w-52 flex flex-col border-l border-base-300/60 h-full overflow-y-auto"
|
||||||
style="background: oklch(4% 0 0);"
|
style="background: oklch(4% 0 0);"
|
||||||
>
|
>
|
||||||
<div class="border-b border-base-300/40">
|
<div class="border-b border-base-300/40">
|
||||||
@@ -291,6 +315,10 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
|||||||
<p class="font-mono text-[9px] text-base-content/20">no linked notes</p>
|
<p class="font-mono text-[9px] text-base-content/20">no linked notes</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div class="px-4 py-6">
|
||||||
|
<Author />
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -462,6 +490,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
|||||||
const graphDrawer = document.getElementById("graph-drawer") as HTMLInputElement | null;
|
const graphDrawer = document.getElementById("graph-drawer") as HTMLInputElement | null;
|
||||||
if (!graphDrawer) return;
|
if (!graphDrawer) return;
|
||||||
|
|
||||||
|
// On non-xl: let DaisyUI overlay work via checkbox
|
||||||
function onGraphDrawerChange() {
|
function onGraphDrawerChange() {
|
||||||
if (graphDrawer!.checked) {
|
if (graphDrawer!.checked) {
|
||||||
requestAnimationFrame(() => { stopGraph = startGraph() ?? null; });
|
requestAnimationFrame(() => { stopGraph = startGraph() ?? null; });
|
||||||
@@ -471,16 +500,35 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
|||||||
}
|
}
|
||||||
graphDrawer.addEventListener("change", onGraphDrawerChange);
|
graphDrawer.addEventListener("change", onGraphDrawerChange);
|
||||||
|
|
||||||
|
const outerDrawer = graphDrawer.closest<HTMLElement>(".drawer.drawer-end");
|
||||||
const xlQuery = window.matchMedia("(min-width: 1280px)");
|
const xlQuery = window.matchMedia("(min-width: 1280px)");
|
||||||
if (xlQuery.matches && !graphDrawer.checked) {
|
|
||||||
graphDrawer.checked = true;
|
function setXlSidebar(open: boolean) {
|
||||||
onGraphDrawerChange();
|
if (!outerDrawer) return;
|
||||||
}
|
if (open) {
|
||||||
xlQuery.addEventListener("change", (e) => {
|
outerDrawer.classList.add("xl:drawer-open");
|
||||||
if (!e.matches && graphDrawer.checked) {
|
requestAnimationFrame(() => { stopGraph = startGraph() ?? null; });
|
||||||
graphDrawer.checked = false;
|
} else {
|
||||||
|
outerDrawer.classList.remove("xl:drawer-open");
|
||||||
if (stopGraph) { stopGraph(); stopGraph = null; }
|
if (stopGraph) { stopGraph(); stopGraph = null; }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On xl: toggle the class instead of the checkbox (avoids DaisyUI overlay + scroll lock)
|
||||||
|
const graphToggle = document.getElementById("graph-toggle");
|
||||||
|
graphToggle?.addEventListener("click", (e) => {
|
||||||
|
if (!xlQuery.matches) return;
|
||||||
|
e.preventDefault();
|
||||||
|
setXlSidebar(!outerDrawer?.classList.contains("xl:drawer-open"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-open on xl, close when leaving xl
|
||||||
|
if (xlQuery.matches) {
|
||||||
|
outerDrawer?.classList.add("xl:drawer-open");
|
||||||
|
requestAnimationFrame(() => { stopGraph = startGraph() ?? null; });
|
||||||
|
}
|
||||||
|
xlQuery.addEventListener("change", (e) => {
|
||||||
|
if (!e.matches) setXlSidebar(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!document.getElementById("heading-anchor-styles")) {
|
if (!document.getElementById("heading-anchor-styles")) {
|
||||||
@@ -533,12 +581,20 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
|||||||
document.querySelectorAll<HTMLInputElement>("[data-search]").forEach((input) => {
|
document.querySelectorAll<HTMLInputElement>("[data-search]").forEach((input) => {
|
||||||
input.addEventListener("input", (e) => {
|
input.addEventListener("input", (e) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
const search = target.value.toLowerCase().trim();
|
const raw = target.value.toLowerCase().trim();
|
||||||
document.querySelectorAll<HTMLInputElement>("[data-search]").forEach((o) => {
|
document.querySelectorAll<HTMLInputElement>("[data-search]").forEach((o) => {
|
||||||
if (o !== target) o.value = target.value;
|
if (o !== target) o.value = target.value;
|
||||||
});
|
});
|
||||||
|
const isTag = raw.startsWith("#");
|
||||||
|
const search = isTag ? raw.slice(1) : raw;
|
||||||
navItems.forEach((item) => {
|
navItems.forEach((item) => {
|
||||||
const match = !search || (item.dataset.title ?? "").includes(search) || (item.dataset.tags ?? "").includes(search);
|
const title = item.dataset.title ?? "";
|
||||||
|
const tags = item.dataset.tags ? item.dataset.tags.split(",") : [];
|
||||||
|
const match = !search || (
|
||||||
|
isTag
|
||||||
|
? tags.some((t) => t.includes(search))
|
||||||
|
: title.includes(search) || tags.join(",").includes(search)
|
||||||
|
);
|
||||||
item.style.display = match ? "" : "none";
|
item.style.display = match ? "" : "none";
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ const sortedNotes = notes.sort(
|
|||||||
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime()
|
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
const categories = [...new Set(notes.map((n) => n.data.category))].sort();
|
function getCategory(n: { id: string; data: { category?: string } }): string {
|
||||||
|
if (n.data.category) return n.data.category;
|
||||||
|
const parts = n.id.split("/");
|
||||||
|
return parts.length > 1 ? parts[0] : "General";
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = [...new Set(notes.map(getCategory))].sort();
|
||||||
|
|
||||||
const searchIndex = Object.fromEntries(
|
const searchIndex = Object.fromEntries(
|
||||||
sortedNotes.map((n) => [
|
sortedNotes.map((n) => [
|
||||||
@@ -53,7 +59,7 @@ const searchIndex = Object.fromEntries(
|
|||||||
<div id="notes-container" class="space-y-12">
|
<div id="notes-container" class="space-y-12">
|
||||||
{
|
{
|
||||||
categories.map((cat) => {
|
categories.map((cat) => {
|
||||||
const catNotes = sortedNotes.filter((n) => n.data.category === cat);
|
const catNotes = sortedNotes.filter((n) => getCategory(n) === cat);
|
||||||
return (
|
return (
|
||||||
<section data-category={cat.toLowerCase()}>
|
<section data-category={cat.toLowerCase()}>
|
||||||
<div class="flex items-baseline gap-3 mb-4">
|
<div class="flex items-baseline gap-3 mb-4">
|
||||||
@@ -136,7 +142,7 @@ const searchIndex = Object.fromEntries(
|
|||||||
const tags = card.dataset.tags ? card.dataset.tags.split(",") : [];
|
const tags = card.dataset.tags ? card.dataset.tags.split(",") : [];
|
||||||
const show = !query || (
|
const show = !query || (
|
||||||
isTag
|
isTag
|
||||||
? tags.some((t) => t.includes(query))
|
? tags.some((t) => t.includes(query)) || (searchIndex[id] ?? "").includes(`#${query}`)
|
||||||
: (searchIndex[id] ?? "").includes(query)
|
: (searchIndex[id] ?? "").includes(query)
|
||||||
);
|
);
|
||||||
card.style.display = show ? "" : "none";
|
card.style.display = show ? "" : "none";
|
||||||
|
|||||||
Reference in New Issue
Block a user