mirror of
https://github.com/anotherhadi/blog.git
synced 2026-05-20 05:32:32 +02:00
@@ -0,0 +1,173 @@
|
||||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import { ChevronRight, Shield } from "@lucide/astro";
|
||||
|
||||
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((n) => n.data.category))].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">
|
||||
<div class="flex items-center justify-center gap-2 mb-4">
|
||||
<Shield size={20} class="text-primary/60" />
|
||||
<span class="font-mono text-xs text-primary/60 tracking-widest uppercase">security notes</span>
|
||||
</div>
|
||||
<h1 class="text-4xl sm:text-5xl font-bold mb-4">Notes</h1>
|
||||
<p class="text-base-content/50 max-w-md mx-auto">
|
||||
Reference sheets on cybersecurity tools and techniques.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-12 max-w-sm mx-auto">
|
||||
<div class="flex items-center gap-2 border border-base-300/60 px-3 py-2 bg-base-200/30 focus-within:border-primary/40 transition-colors">
|
||||
<span class="font-mono text-sm text-base-content/25">›</span>
|
||||
<input
|
||||
data-search
|
||||
type="text"
|
||||
placeholder="search or #tag..."
|
||||
class="bg-transparent font-mono text-sm text-base-content/70 placeholder:text-base-content/25 outline-none w-full"
|
||||
/>
|
||||
</div>
|
||||
<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) => n.data.category === 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 items-baseline gap-3 mb-0.5">
|
||||
<span class="font-semibold text-sm group-hover:text-primary transition-colors">
|
||||
{n.data.title}
|
||||
</span>
|
||||
<span class="hidden sm:block 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="font-mono text-[10px] px-1.5 py-0.5 border border-base-300/40 text-base-content/25">
|
||||
{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)
|
||||
);
|
||||
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>
|
||||
Reference in New Issue
Block a user