mirror of
https://github.com/anotherhadi/blog.git
synced 2026-05-20 05:32:32 +02:00
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { slide } from "svelte/transition";
|
import { slide } from "svelte/transition";
|
||||||
|
import { untrack } from "svelte";
|
||||||
|
|
||||||
interface Note {
|
interface Note {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -9,11 +10,12 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
notes: Note[];
|
notes: Note[];
|
||||||
currentEntry: Note;
|
currentEntry?: Note;
|
||||||
|
currentCategory?: string;
|
||||||
categories: string[];
|
categories: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { notes, currentEntry, categories }: Props = $props();
|
const { notes, currentEntry, currentCategory, categories }: Props = $props();
|
||||||
|
|
||||||
let search = $state("");
|
let search = $state("");
|
||||||
|
|
||||||
@@ -46,8 +48,12 @@
|
|||||||
: title.includes(term) || tags.join(",").includes(term);
|
: title.includes(term) || tags.join(",").includes(term);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeCategory = $derived(
|
||||||
|
currentCategory ?? (currentEntry ? getCategory(currentEntry) : null),
|
||||||
|
);
|
||||||
|
|
||||||
let openCategories = $state<string[]>(
|
let openCategories = $state<string[]>(
|
||||||
categories.filter((c) => c === getCategory(currentEntry)),
|
untrack(() => categories.filter((c) => c === activeCategory)),
|
||||||
);
|
);
|
||||||
|
|
||||||
function toggle(cat: string) {
|
function toggle(cat: string) {
|
||||||
@@ -99,32 +105,42 @@
|
|||||||
(n) => getCategory(n) === cat && matchesSearch(n),
|
(n) => getCategory(n) === cat && matchesSearch(n),
|
||||||
)}
|
)}
|
||||||
{#if catNotes.length > 0 || !search}
|
{#if catNotes.length > 0 || !search}
|
||||||
|
{@const isFolder = notes.some((n) => n.id.includes("/") && getCategory(n) === cat)}
|
||||||
<div>
|
<div>
|
||||||
<!-- Category header -->
|
<!-- Category header -->
|
||||||
<button
|
<div class="flex items-center w-full">
|
||||||
onclick={() => toggle(cat)}
|
<button
|
||||||
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-md hover:bg-base-200/40 transition-colors duration-150 group"
|
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
|
||||||
</svg>
|
class="w-3 h-3 text-base-content/35 shrink-0 transition-transform duration-200"
|
||||||
<span class="text-primary/50 font-mono text-xs shrink-0">/</span>
|
class:rotate-90={openCategories.includes(cat)}
|
||||||
<span
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="font-bold tracking-tight text-sm truncate text-base-content/80"
|
viewBox="0 0 24 24"
|
||||||
>
|
fill="none"
|
||||||
{cat}
|
stroke="currentColor"
|
||||||
</span>
|
stroke-width="2.5"
|
||||||
</button>
|
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>
|
||||||
|
|
||||||
<!-- Notes list -->
|
<!-- Notes list -->
|
||||||
{#if openCategories.includes(cat)}
|
{#if openCategories.includes(cat)}
|
||||||
@@ -138,12 +154,12 @@
|
|||||||
href={`/notes/${note.id}`}
|
href={`/notes/${note.id}`}
|
||||||
title={note.data.title}
|
title={note.data.title}
|
||||||
class="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-mono truncate transition-colors duration-150
|
class="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-mono truncate transition-colors duration-150
|
||||||
{note.id === currentEntry.id
|
{currentEntry && note.id === currentEntry.id
|
||||||
? 'text-primary/90 bg-primary/10'
|
? 'text-primary/90 bg-primary/10'
|
||||||
: 'text-base-content/45 hover:text-base-content/80 hover:bg-base-200/40'}"
|
: 'text-base-content/45 hover:text-base-content/80 hover:bg-base-200/40'}"
|
||||||
>
|
>
|
||||||
<span class="shrink-0 font-mono text-base-content/20">
|
<span class="shrink-0 font-mono text-base-content/20">
|
||||||
{note.id === currentEntry.id ? "▸" : "–"}
|
{currentEntry && note.id === currentEntry.id ? "▸" : "–"}
|
||||||
</span>
|
</span>
|
||||||
<span class="truncate">{note.data.title}</span>
|
<span class="truncate">{note.data.title}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import NoteTOC from "../../components/NoteTOC.astro";
|
|||||||
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
|
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
|
||||||
import NoteGraphSidebar from "../../components/NoteGraphSidebar.astro";
|
import NoteGraphSidebar from "../../components/NoteGraphSidebar.astro";
|
||||||
import NoteVars from "../../components/NoteVars.svelte";
|
import NoteVars from "../../components/NoteVars.svelte";
|
||||||
import {
|
import { getCategory, extractLinks, extractHeadings } from "../../utils/notes";
|
||||||
getCategory,
|
|
||||||
extractLinks,
|
|
||||||
extractHeadings,
|
|
||||||
} from "../../utils/notes";
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const notes = await getCollection("notes");
|
const notes = await getCollection("notes");
|
||||||
@@ -41,7 +37,11 @@ const backlinks = allNotes.filter(
|
|||||||
|
|
||||||
const graphNodes = [
|
const graphNodes = [
|
||||||
{ id: entry.id, title: entry.data.title, current: true },
|
{ id: entry.id, title: entry.data.title, current: true },
|
||||||
...forwardLinks.map((n) => ({ id: n.id, title: n.data.title, current: false })),
|
...forwardLinks.map((n) => ({
|
||||||
|
id: n.id,
|
||||||
|
title: n.data.title,
|
||||||
|
current: false,
|
||||||
|
})),
|
||||||
...backlinks
|
...backlinks
|
||||||
.filter((n) => !forwardLinks.some((f) => f.id === n.id))
|
.filter((n) => !forwardLinks.some((f) => f.id === n.id))
|
||||||
.map((n) => ({ id: n.id, title: n.data.title, current: false })),
|
.map((n) => ({ id: n.id, title: n.data.title, current: false })),
|
||||||
@@ -72,7 +72,7 @@ const headings = extractHeadings(entry.body ?? "");
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
title={`${entry.data.title} — Security Notes`}
|
title={`${entry.data.title} - Security Notes`}
|
||||||
description={entry.data.description}
|
description={entry.data.description}
|
||||||
>
|
>
|
||||||
<main class="max-w-screen-2xl mx-auto">
|
<main class="max-w-screen-2xl mx-auto">
|
||||||
@@ -92,12 +92,24 @@ const headings = extractHeadings(entry.body ?? "");
|
|||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="/notes" class="hover:text-base-content/70"
|
||||||
href="/notes"
|
>notes</a
|
||||||
class="hover:text-base-content/70"
|
>
|
||||||
>notes</a>
|
</li>
|
||||||
|
<li>
|
||||||
|
{
|
||||||
|
entry.id.includes("/") ? (
|
||||||
|
<a
|
||||||
|
href={`/notes/${getCategory(entry)}`}
|
||||||
|
class="hover:text-base-content/70"
|
||||||
|
>
|
||||||
|
{getCategory(entry)}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
getCategory(entry)
|
||||||
|
)
|
||||||
|
}
|
||||||
</li>
|
</li>
|
||||||
<li>{getCategory(entry)}</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -196,8 +208,7 @@ const headings = extractHeadings(entry.body ?? "");
|
|||||||
<label
|
<label
|
||||||
for="nav-drawer"
|
for="nav-drawer"
|
||||||
aria-label="close sidebar"
|
aria-label="close sidebar"
|
||||||
class="drawer-overlay"
|
class="drawer-overlay"></label>
|
||||||
></label>
|
|
||||||
<NoteNavSidebar
|
<NoteNavSidebar
|
||||||
client:load
|
client:load
|
||||||
notes={sortedNotes}
|
notes={sortedNotes}
|
||||||
@@ -212,8 +223,7 @@ const headings = extractHeadings(entry.body ?? "");
|
|||||||
<label
|
<label
|
||||||
for="graph-drawer"
|
for="graph-drawer"
|
||||||
aria-label="close sidebar"
|
aria-label="close sidebar"
|
||||||
class="drawer-overlay xl:hidden"
|
class="drawer-overlay xl:hidden"></label>
|
||||||
></label>
|
|
||||||
<NoteGraphSidebar
|
<NoteGraphSidebar
|
||||||
entry={entry}
|
entry={entry}
|
||||||
graphNodes={graphNodes}
|
graphNodes={graphNodes}
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
|
||||||
|
import { getCategory } from "../../utils/notes";
|
||||||
|
import { List } from "@lucide/astro";
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const notes = await getCollection("notes");
|
||||||
|
|
||||||
|
const folderCategories = [
|
||||||
|
...new Set(
|
||||||
|
notes.filter((n) => n.id.includes("/")).map((n) => getCategory(n)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return folderCategories.map((category) => {
|
||||||
|
const allNotes = notes.sort((a, b) =>
|
||||||
|
a.data.title.localeCompare(b.data.title),
|
||||||
|
);
|
||||||
|
const categories = [...new Set(notes.map(getCategory))].sort();
|
||||||
|
return {
|
||||||
|
params: { category },
|
||||||
|
props: {
|
||||||
|
category,
|
||||||
|
categoryNotes: notes
|
||||||
|
.filter((n) => getCategory(n) === category)
|
||||||
|
.sort((a, b) => a.data.title.localeCompare(b.data.title)),
|
||||||
|
allNotes,
|
||||||
|
categories,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { category, categoryNotes, allNotes, categories } = Astro.props;
|
||||||
|
|
||||||
|
if (!categoryNotes) {
|
||||||
|
return new Response(null, { status: 404, statusText: "Not found" });
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.drawer.lg\:drawer-open > .drawer-side {
|
||||||
|
top: 3rem;
|
||||||
|
height: calc(100vh - 3rem);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<Layout
|
||||||
|
title={`${category} - Security Notes`}
|
||||||
|
description={`Notes on ${category}.`}
|
||||||
|
>
|
||||||
|
<main class="max-w-screen-2xl mx-auto">
|
||||||
|
<div class="drawer lg:drawer-open min-h-[calc(100vh-3rem)]">
|
||||||
|
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
|
|
||||||
|
<div class="drawer-content flex flex-col min-w-0">
|
||||||
|
<main class="flex-1 px-4 sm:px-6 lg:px-10 py-6 lg:py-10 min-w-0">
|
||||||
|
<div class="max-w-3xl mx-auto lg:mx-0">
|
||||||
|
<div class="flex items-center justify-between mb-10">
|
||||||
|
<div
|
||||||
|
class="breadcrumbs text-xs font-mono text-base-content/35 p-0"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="/notes" class="hover:text-base-content/70"
|
||||||
|
>notes</a>
|
||||||
|
</li>
|
||||||
|
<li class="text-base-content/60">{category}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
for="nav-drawer"
|
||||||
|
class="btn btn-ghost btn-xs lg:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||||
|
>
|
||||||
|
<List size={11} />
|
||||||
|
nav
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-10">
|
||||||
|
<div class="flex items-baseline gap-3 mb-1">
|
||||||
|
<h1 class="text-4xl sm:text-5xl font-bold">
|
||||||
|
<span class="text-primary/40 font-mono mr-1">/</span>{
|
||||||
|
category
|
||||||
|
}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<p class="font-mono text-xs text-base-content/25 ml-8">
|
||||||
|
{categoryNotes.length} note{
|
||||||
|
categoryNotes.length !== 1 ? "s" : ""
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-base-300/40 mb-1"></div>
|
||||||
|
<ul class="divide-y divide-base-300/20">
|
||||||
|
{
|
||||||
|
categoryNotes.map((note) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={`/notes/${note.id}`}
|
||||||
|
class="group flex items-center gap-4 py-3 hover:bg-base-200/30 px-2 -mx-2 transition-colors"
|
||||||
|
>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex flex-col mb-0.5">
|
||||||
|
<span class="font-semibold text-sm group-hover:text-primary transition-colors">
|
||||||
|
{note.data.title}
|
||||||
|
</span>
|
||||||
|
{note.data.description && (
|
||||||
|
<span class="text-xs text-base-content/35 truncate">
|
||||||
|
{note.data.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{note.data.tags.length > 0 && (
|
||||||
|
<div class="flex flex-wrap gap-1 mt-1">
|
||||||
|
{note.data.tags.map((tag) => (
|
||||||
|
<span class="badge badge-ghost badge-xs font-mono text-base-content/30">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="text-base-content/20 group-hover:text-primary/50 shrink-0 transition-colors"
|
||||||
|
>
|
||||||
|
<path d="m9 18 6-6-6-6" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="border-t border-base-300/30 mt-12 pt-6 flex items-center justify-between font-mono text-[10px] text-base-content/25"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/notes"
|
||||||
|
class="hover:text-base-content/50 transition-colors"
|
||||||
|
>
|
||||||
|
← all notes
|
||||||
|
</a>
|
||||||
|
<a href="/" class="hover:text-base-content/50 transition-colors">
|
||||||
|
~/hadi
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-side z-50">
|
||||||
|
<label
|
||||||
|
for="nav-drawer"
|
||||||
|
aria-label="close sidebar"
|
||||||
|
class="drawer-overlay"></label>
|
||||||
|
<NoteNavSidebar
|
||||||
|
client:load
|
||||||
|
notes={allNotes}
|
||||||
|
currentCategory={category}
|
||||||
|
categories={categories}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
@@ -22,7 +22,7 @@ const searchNotes = sortedNotes.map((n) => ({
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
title="Security Notes — Another Hadi"
|
title="Security Notes - Another Hadi"
|
||||||
description="Reference notes on cybersecurity tools and techniques."
|
description="Reference notes on cybersecurity tools and techniques."
|
||||||
>
|
>
|
||||||
<main class="max-w-4xl mx-auto px-4 py-16 sm:py-20">
|
<main class="max-w-4xl mx-auto px-4 py-16 sm:py-20">
|
||||||
|
|||||||
Reference in New Issue
Block a user