mirror of
https://github.com/anotherhadi/blog.git
synced 2026-05-20 05:32:32 +02:00
f00515e4c3
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
175 lines
6.1 KiB
Plaintext
175 lines
6.1 KiB
Plaintext
---
|
||
import Layout from "../../layouts/Layout.astro";
|
||
import { getCollection } from "astro:content";
|
||
import { ChevronRight, Shield } from "@lucide/astro";
|
||
import { getCategory } from "../../utils/notes";
|
||
|
||
const notes = await getCollection("notes");
|
||
const sortedNotes = notes.sort(
|
||
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(),
|
||
);
|
||
|
||
const categories = [...new Set(notes.map(getCategory))].sort();
|
||
|
||
const searchIndex = Object.fromEntries(
|
||
sortedNotes.map((n) => [
|
||
n.id,
|
||
[n.data.title, n.data.description, n.body ?? ""].join(" ").toLowerCase(),
|
||
]),
|
||
);
|
||
---
|
||
|
||
<Layout
|
||
title="Security Notes — Another Hadi"
|
||
description="Reference notes on cybersecurity tools and techniques."
|
||
>
|
||
<main class="max-w-4xl mx-auto px-4 py-16 sm:py-20">
|
||
<div class="text-center mb-12">
|
||
<h1 class="text-4xl sm:text-5xl font-bold mb-4">Notes</h1>
|
||
<p class="text-xl text-base-content/70">
|
||
Reference sheets on cybersecurity tools and techniques.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="mb-12 max-w-sm mx-auto">
|
||
<label class="input w-full">
|
||
<span class="text-base-content/25">›</span>
|
||
<input data-search type="text" placeholder="search or #tag..." />
|
||
</label>
|
||
<p class="font-mono text-[10px] text-base-content/20 mt-1.5 text-center">
|
||
use #tag to filter by tag
|
||
</p>
|
||
</div>
|
||
|
||
<div id="notes-container" class="space-y-12">
|
||
{
|
||
categories.map((cat) => {
|
||
const catNotes = sortedNotes.filter((n) => getCategory(n) === cat);
|
||
return (
|
||
<section data-category={cat.toLowerCase()}>
|
||
<div class="flex items-baseline gap-3 mb-4">
|
||
<h2 class="text-xl font-bold tracking-tight">
|
||
<span class="text-primary/50 font-mono mr-1">/</span>
|
||
{cat}
|
||
</h2>
|
||
<span class="font-mono text-xs text-base-content/25">
|
||
{catNotes.length} note{catNotes.length !== 1 ? "s" : ""}
|
||
</span>
|
||
</div>
|
||
<div class="border-t border-base-300/40 mb-1" />
|
||
|
||
<ul class="divide-y divide-base-300/20">
|
||
{catNotes.map((n) => (
|
||
<li>
|
||
<a
|
||
href={`/notes/${n.id}`}
|
||
class="note-card group flex items-center gap-4 py-3 hover:bg-base-200/30 px-2 -mx-2 transition-colors"
|
||
data-id={n.id}
|
||
data-tags={n.data.tags.join(",")}
|
||
>
|
||
<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">
|
||
{n.data.title}
|
||
</span>
|
||
{n.data.description && (
|
||
<span class="text-xs text-base-content/35 truncate">
|
||
{n.data.description}
|
||
</span>
|
||
)}
|
||
</div>
|
||
{n.data.tags.length > 0 && (
|
||
<div class="flex flex-wrap gap-1 mt-1">
|
||
{n.data.tags.map((tag) => (
|
||
<span class="badge badge-ghost badge-xs font-mono text-base-content/30">
|
||
{tag}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<ChevronRight
|
||
size={14}
|
||
class="text-base-content/20 group-hover:text-primary/50 shrink-0 transition-colors"
|
||
/>
|
||
</a>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</section>
|
||
);
|
||
})
|
||
}
|
||
</div>
|
||
|
||
<div
|
||
id="empty-state"
|
||
class="hidden text-center py-20 font-mono text-sm text-base-content/25"
|
||
>
|
||
no results.
|
||
</div>
|
||
|
||
<p class="text-center font-mono text-xs text-base-content/20 mt-16">
|
||
<span id="note-count">{notes.length}</span> note{
|
||
notes.length !== 1 ? "s" : ""
|
||
} total
|
||
</p>
|
||
</main>
|
||
|
||
<script is:inline define:vars={{ searchIndex }}>
|
||
function init() {
|
||
const noteCards = document.querySelectorAll(".note-card");
|
||
const sections = document.querySelectorAll("[data-category]");
|
||
const emptyState = document.getElementById("empty-state");
|
||
const noteCount = document.getElementById("note-count");
|
||
const container = document.getElementById("notes-container");
|
||
|
||
function filter(raw) {
|
||
const isTag = raw.startsWith("#");
|
||
const query = isTag ? raw.slice(1) : raw;
|
||
|
||
let visible = 0;
|
||
noteCards.forEach((card) => {
|
||
const id = card.dataset.id ?? "";
|
||
const tags = card.dataset.tags ? card.dataset.tags.split(",") : [];
|
||
const show =
|
||
!query ||
|
||
(isTag
|
||
? tags.some((t) => t.includes(query)) ||
|
||
(searchIndex[id] ?? "").includes(`#${query}`)
|
||
: (searchIndex[id] ?? "").includes(query));
|
||
card.style.display = show ? "" : "none";
|
||
if (show) visible++;
|
||
});
|
||
|
||
sections.forEach((section) => {
|
||
const anyVisible = [...section.querySelectorAll(".note-card")].some(
|
||
(c) => c.style.display !== "none",
|
||
);
|
||
section.style.display = anyVisible ? "" : "none";
|
||
});
|
||
|
||
noteCount.textContent = String(visible);
|
||
container.style.display = visible > 0 ? "" : "none";
|
||
emptyState.classList.toggle("hidden", visible > 0);
|
||
}
|
||
|
||
document.querySelectorAll("[data-search]").forEach((input) => {
|
||
input.addEventListener("input", (e) => {
|
||
filter(e.target.value.toLowerCase().trim());
|
||
});
|
||
});
|
||
|
||
const urlTag = new URLSearchParams(window.location.search).get("tag");
|
||
if (urlTag) {
|
||
document.querySelectorAll("[data-search]").forEach((i) => {
|
||
i.value = `#${urlTag}`;
|
||
});
|
||
filter(`#${urlTag}`);
|
||
}
|
||
}
|
||
|
||
document.addEventListener("astro:page-load", init);
|
||
</script>
|
||
</Layout>
|