mirror of
https://github.com/anotherhadi/blog.git
synced 2026-05-20 05:32:32 +02:00
format
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
+79
-36
@@ -3,57 +3,85 @@
|
|||||||
(function oneko() {
|
(function oneko() {
|
||||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||||
|
|
||||||
const SIZE = 32;
|
const SIZE = 32;
|
||||||
const SPEED = 10;
|
const SPEED = 10;
|
||||||
|
|
||||||
const spriteSets = {
|
const spriteSets = {
|
||||||
idle: [[-3, -3]],
|
idle: [[-3, -3]],
|
||||||
alert: [[-7, -3]],
|
alert: [[-7, -3]],
|
||||||
scratchSelf: [[-5, 0], [-6, 0], [-7, 0]],
|
scratchSelf: [
|
||||||
scratchWallE:[[-2, -2], [-2, -3]],
|
[-5, 0],
|
||||||
scratchWallW:[[-4, 0], [-4, -1]],
|
[-6, 0],
|
||||||
tired: [[-3, -2]],
|
[-7, 0],
|
||||||
sleeping: [[-2, 0], [-2, -1]],
|
],
|
||||||
E: [[-3, 0], [-3, -1]],
|
scratchWallE: [
|
||||||
W: [[-4, -2], [-4, -3]],
|
[-2, -2],
|
||||||
|
[-2, -3],
|
||||||
|
],
|
||||||
|
scratchWallW: [
|
||||||
|
[-4, 0],
|
||||||
|
[-4, -1],
|
||||||
|
],
|
||||||
|
tired: [[-3, -2]],
|
||||||
|
sleeping: [
|
||||||
|
[-2, 0],
|
||||||
|
[-2, -1],
|
||||||
|
],
|
||||||
|
E: [
|
||||||
|
[-3, 0],
|
||||||
|
[-3, -1],
|
||||||
|
],
|
||||||
|
W: [
|
||||||
|
[-4, -2],
|
||||||
|
[-4, -3],
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const track = document.getElementById("oneko-track");
|
const track = document.getElementById("oneko-track");
|
||||||
if (!track) return;
|
if (!track) return;
|
||||||
|
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.id = "oneko";
|
el.id = "oneko";
|
||||||
el.ariaHidden = "true";
|
el.ariaHidden = "true";
|
||||||
el.style.width = `${SIZE}px`;
|
el.style.width = `${SIZE}px`;
|
||||||
el.style.height = `${SIZE}px`;
|
el.style.height = `${SIZE}px`;
|
||||||
el.style.position = "absolute";
|
el.style.position = "absolute";
|
||||||
el.style.bottom = "0";
|
el.style.bottom = "0";
|
||||||
el.style.pointerEvents = "none";
|
el.style.pointerEvents = "none";
|
||||||
el.style.imageRendering = "pixelated";
|
el.style.imageRendering = "pixelated";
|
||||||
el.style.zIndex = "2147483647";
|
el.style.zIndex = "2147483647";
|
||||||
el.style.backgroundImage= "url(/oneko.gif)";
|
el.style.backgroundImage = "url(/oneko.gif)";
|
||||||
track.appendChild(el);
|
track.appendChild(el);
|
||||||
|
|
||||||
function maxX() { return track.offsetWidth - SIZE; }
|
function maxX() {
|
||||||
function clamp(v,lo,hi) { return Math.max(lo, Math.min(hi, v)); }
|
return track.offsetWidth - SIZE;
|
||||||
function randomTarget() { return Math.random() * maxX(); }
|
}
|
||||||
|
function clamp(v, lo, hi) {
|
||||||
|
return Math.max(lo, Math.min(hi, v));
|
||||||
|
}
|
||||||
|
function randomTarget() {
|
||||||
|
return Math.random() * maxX();
|
||||||
|
}
|
||||||
|
|
||||||
function setSprite(name, frame) {
|
function setSprite(name, frame) {
|
||||||
const s = spriteSets[name][frame % spriteSets[name].length];
|
const s = spriteSets[name][frame % spriteSets[name].length];
|
||||||
el.style.backgroundPosition = `${s[0] * SIZE}px ${s[1] * SIZE}px`;
|
el.style.backgroundPosition = `${s[0] * SIZE}px ${s[1] * SIZE}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let posX = randomTarget();
|
let posX = randomTarget();
|
||||||
let targetX = posX;
|
let targetX = posX;
|
||||||
el.style.left = `${posX}px`;
|
el.style.left = `${posX}px`;
|
||||||
|
|
||||||
let frameCount = 0;
|
let frameCount = 0;
|
||||||
let idleTime = 0;
|
let idleTime = 0;
|
||||||
let idleAnim = null;
|
let idleAnim = null;
|
||||||
let idleAnimFrame = 0;
|
let idleAnimFrame = 0;
|
||||||
let lastTs = null;
|
let lastTs = null;
|
||||||
|
|
||||||
function resetIdle() { idleAnim = null; idleAnimFrame = 0; }
|
function resetIdle() {
|
||||||
|
idleAnim = null;
|
||||||
|
idleAnimFrame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
function idle() {
|
function idle() {
|
||||||
idleTime++;
|
idleTime++;
|
||||||
@@ -65,16 +93,23 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (idleTime > 15 && idleAnim == null && Math.floor(Math.random() * 180) === 0) {
|
if (
|
||||||
|
idleTime > 15 &&
|
||||||
|
idleAnim == null &&
|
||||||
|
Math.floor(Math.random() * 180) === 0
|
||||||
|
) {
|
||||||
const opts = ["sleeping", "scratchSelf"];
|
const opts = ["sleeping", "scratchSelf"];
|
||||||
if (posX <= SIZE) opts.push("scratchWallW");
|
if (posX <= SIZE) opts.push("scratchWallW");
|
||||||
if (posX >= maxX() - SIZE) opts.push("scratchWallE");
|
if (posX >= maxX() - SIZE) opts.push("scratchWallE");
|
||||||
idleAnim = opts[Math.floor(Math.random() * opts.length)];
|
idleAnim = opts[Math.floor(Math.random() * opts.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (idleAnim) {
|
switch (idleAnim) {
|
||||||
case "sleeping":
|
case "sleeping":
|
||||||
if (idleAnimFrame < 8) { setSprite("tired", 0); break; }
|
if (idleAnimFrame < 8) {
|
||||||
|
setSprite("tired", 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
setSprite("sleeping", Math.floor(idleAnimFrame / 4));
|
setSprite("sleeping", Math.floor(idleAnimFrame / 4));
|
||||||
if (idleAnimFrame > 192) resetIdle();
|
if (idleAnimFrame > 192) resetIdle();
|
||||||
break;
|
break;
|
||||||
@@ -119,7 +154,10 @@
|
|||||||
function loop(ts) {
|
function loop(ts) {
|
||||||
if (!el.isConnected) return;
|
if (!el.isConnected) return;
|
||||||
if (!lastTs) lastTs = ts;
|
if (!lastTs) lastTs = ts;
|
||||||
if (ts - lastTs > 100) { lastTs = ts; frame(); }
|
if (ts - lastTs > 100) {
|
||||||
|
lastTs = ts;
|
||||||
|
frame();
|
||||||
|
}
|
||||||
window.requestAnimationFrame(loop);
|
window.requestAnimationFrame(loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,14 +166,19 @@
|
|||||||
const newWidth = track.offsetWidth;
|
const newWidth = track.offsetWidth;
|
||||||
if (lastTrackWidth > 0) {
|
if (lastTrackWidth > 0) {
|
||||||
const ratio = newWidth / lastTrackWidth;
|
const ratio = newWidth / lastTrackWidth;
|
||||||
posX = clamp(posX * ratio, 0, maxX());
|
posX = clamp(posX * ratio, 0, maxX());
|
||||||
targetX = clamp(targetX * ratio, 0, maxX());
|
targetX = clamp(targetX * ratio, 0, maxX());
|
||||||
el.style.left = `${posX}px`;
|
el.style.left = `${posX}px`;
|
||||||
}
|
}
|
||||||
lastTrackWidth = newWidth;
|
lastTrackWidth = newWidth;
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => { targetX = randomTarget(); }, 800 + Math.random() * 1500);
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
targetX = randomTarget();
|
||||||
|
},
|
||||||
|
800 + Math.random() * 1500,
|
||||||
|
);
|
||||||
|
|
||||||
window.requestAnimationFrame(loop);
|
window.requestAnimationFrame(loop);
|
||||||
})();
|
})();
|
||||||
|
|||||||
+7
-14
@@ -53,28 +53,24 @@ async function checkMirrors(repoName: string): Promise<RepoMirrors> {
|
|||||||
export async function fetchGiteaRepos(): Promise<GiteaRepoWithMirrors[]> {
|
export async function fetchGiteaRepos(): Promise<GiteaRepoWithMirrors[]> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${GITEA_BASE}/api/v1/users/${GITEA_USER}/repos?limit=50&page=1`
|
`${GITEA_BASE}/api/v1/users/${GITEA_USER}/repos?limit=50&page=1`,
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new Error(`Gitea API: ${res.status}`);
|
if (!res.ok) throw new Error(`Gitea API: ${res.status}`);
|
||||||
|
|
||||||
const repos: GiteaRepo[] = await res.json();
|
const repos: GiteaRepo[] = await res.json();
|
||||||
|
|
||||||
const filtered = repos
|
const filtered = repos
|
||||||
.filter((r) =>
|
.filter((r) => !r.fork && !r.private && !SKIP_REPOS.includes(r.name))
|
||||||
!r.fork &&
|
|
||||||
!r.private &&
|
|
||||||
!SKIP_REPOS.includes(r.name)
|
|
||||||
)
|
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
|
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const reposWithMirrors = await Promise.all(
|
const reposWithMirrors = await Promise.all(
|
||||||
filtered.map(async (repo) => ({
|
filtered.map(async (repo) => ({
|
||||||
...repo,
|
...repo,
|
||||||
mirrors: await checkMirrors(repo.name),
|
mirrors: await checkMirrors(repo.name),
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
return reposWithMirrors;
|
return reposWithMirrors;
|
||||||
@@ -91,18 +87,15 @@ export function getBannerUrl(repo: GiteaRepo): string {
|
|||||||
async function main() {
|
async function main() {
|
||||||
console.log("Fetching repos from Gitea...");
|
console.log("Fetching repos from Gitea...");
|
||||||
const rawRepos = await fetchGiteaRepos();
|
const rawRepos = await fetchGiteaRepos();
|
||||||
const repos = rawRepos.map(repo => ({
|
const repos = rawRepos.map((repo) => ({
|
||||||
...repo,
|
...repo,
|
||||||
banner_url: `${GITEA_BASE}/${repo.full_name}/raw/branch/main/.github/assets/banner.png`
|
banner_url: `${GITEA_BASE}/${repo.full_name}/raw/branch/main/.github/assets/banner.png`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const dataDir = join(process.cwd(), "src/data");
|
const dataDir = join(process.cwd(), "src/data");
|
||||||
await mkdir(dataDir, { recursive: true });
|
await mkdir(dataDir, { recursive: true });
|
||||||
|
|
||||||
await Bun.write(
|
await Bun.write(join(dataDir, "repos.json"), JSON.stringify(repos, null, 2));
|
||||||
join(dataDir, "repos.json"),
|
|
||||||
JSON.stringify(repos, null, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Saved ${repos.length} repos to src/data/repos.json`);
|
console.log(`Saved ${repos.length} repos to src/data/repos.json`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
---
|
---
|
||||||
import { Image } from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
const avatar = "/avatar.jpg";
|
const avatar = "/avatar.jpg";
|
||||||
const username = "anotherhadi"
|
const username = "anotherhadi";
|
||||||
const bio = "Infosec engineer."
|
const bio = "Infosec engineer.";
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="flex flex-wrap 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"
|
||||||
>
|
>
|
||||||
<Image src={avatar} alt="anotherhadi avatar" class="rounded-full m-auto" width={36} height={36}/>
|
<Image
|
||||||
|
src={avatar}
|
||||||
|
alt="anotherhadi avatar"
|
||||||
|
class="rounded-full m-auto"
|
||||||
|
width={36}
|
||||||
|
height={36}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold"><a href="/"><span class="text-base-content/40">@</span>{username}</a></p>
|
<p class="text-sm font-semibold">
|
||||||
|
<a href="/"><span class="text-base-content/40">@</span>{username}</a>
|
||||||
|
</p>
|
||||||
<p class="text-xs text-base-content/60">{bio}</p>
|
<p class="text-xs text-base-content/60">{bio}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ interface Props {
|
|||||||
const { repo } = Astro.props;
|
const { repo } = Astro.props;
|
||||||
|
|
||||||
const platforms = [
|
const platforms = [
|
||||||
...(repo.mirrors.github ? [{ label: "GitHub", url: repo.mirrors.github }] : []),
|
...(repo.mirrors.github
|
||||||
...(repo.mirrors.gitlab ? [{ label: "GitLab", url: repo.mirrors.gitlab }] : []),
|
? [{ label: "GitHub", url: repo.mirrors.github }]
|
||||||
|
: []),
|
||||||
|
...(repo.mirrors.gitlab
|
||||||
|
? [{ label: "GitLab", url: repo.mirrors.gitlab }]
|
||||||
|
: []),
|
||||||
{ label: "Gitea", url: repo.html_url },
|
{ label: "Gitea", url: repo.html_url },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -46,64 +50,70 @@ const hasMultiplePlatforms = platforms.length > 1;
|
|||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{repo.description && (
|
{repo.description && <p class="text-base-content/80">{repo.description}</p>}
|
||||||
<p class="text-base-content/80">{repo.description}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2 mt-2">
|
<div class="flex flex-wrap gap-2 mt-2">
|
||||||
{repo.topics.map((topic) => (
|
{
|
||||||
<span class="badge badge-sm rounded-sm badge-soft badge-accent">
|
repo.topics.map((topic) => (
|
||||||
{topic}
|
<span class="badge badge-sm rounded-sm badge-soft badge-accent">
|
||||||
</span>
|
{topic}
|
||||||
))}
|
</span>
|
||||||
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions justify-end mt-4 gap-2">
|
<div class="card-actions justify-end mt-4 gap-2">
|
||||||
{repo.website && (
|
{
|
||||||
|
repo.website && (
|
||||||
<a
|
<a
|
||||||
href={repo.website}
|
href={repo.website}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="btn btn-soft btn-sm gap-1"
|
class="btn btn-soft btn-sm gap-1"
|
||||||
>
|
|
||||||
<ExternalLink class="size-4" />
|
|
||||||
Website
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasMultiplePlatforms ? (
|
|
||||||
<div class="dropdown dropdown-end">
|
|
||||||
<div tabindex="0" role="button" class="btn btn-primary btn-sm gap-1">
|
|
||||||
<ExternalLink class="size-4" />
|
|
||||||
View Source
|
|
||||||
<ChevronDown class="size-3" />
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
tabindex="0"
|
|
||||||
class="dropdown-content menu bg-base-100 rounded-xl z-10 w-36 p-1.5 shadow border border-base-200 text-sm"
|
|
||||||
>
|
>
|
||||||
{platforms.map(({ label, url }) => (
|
<ExternalLink class="size-4" />
|
||||||
<li>
|
Website
|
||||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
</a>
|
||||||
{label}
|
)
|
||||||
</a>
|
}
|
||||||
</li>
|
|
||||||
))}
|
{
|
||||||
</ul>
|
hasMultiplePlatforms ? (
|
||||||
</div>
|
<div class="dropdown dropdown-end">
|
||||||
) : (
|
<div
|
||||||
|
tabindex="0"
|
||||||
<a
|
role="button"
|
||||||
href={repo.html_url}
|
class="btn btn-primary btn-sm gap-1"
|
||||||
target="_blank"
|
>
|
||||||
rel="noopener noreferrer"
|
<ExternalLink class="size-4" />
|
||||||
class="btn btn-primary btn-sm gap-1"
|
View Source
|
||||||
>
|
<ChevronDown class="size-3" />
|
||||||
<ExternalLink class="size-4" />
|
</div>
|
||||||
View on Gitea
|
<ul
|
||||||
</a>
|
tabindex="0"
|
||||||
)}
|
class="dropdown-content menu bg-base-100 rounded-xl z-10 w-36 p-1.5 shadow border border-base-200 text-sm"
|
||||||
|
>
|
||||||
|
{platforms.map(({ label, url }) => (
|
||||||
|
<li>
|
||||||
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={repo.html_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="btn btn-primary btn-sm gap-1"
|
||||||
|
>
|
||||||
|
<ExternalLink class="size-4" />
|
||||||
|
View on Gitea
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
+36
-22
@@ -25,8 +25,16 @@ interface Props {
|
|||||||
rssFeed?: string;
|
rssFeed?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, title, description, avatar, location, socialLinks, gpgKey, rssFeed } =
|
const {
|
||||||
Astro.props;
|
name,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
avatar,
|
||||||
|
location,
|
||||||
|
socialLinks,
|
||||||
|
gpgKey,
|
||||||
|
rssFeed,
|
||||||
|
} = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="hero min-h-[65vh]">
|
<section class="hero min-h-[65vh]">
|
||||||
@@ -80,34 +88,40 @@ const { name, title, description, avatar, location, socialLinks, gpgKey, rssFeed
|
|||||||
class="btn btn-circle btn-ghost"
|
class="btn btn-circle btn-ghost"
|
||||||
aria-label="Gitlab"
|
aria-label="Gitlab"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
role="img"
|
||||||
<path d="m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z"/>
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{socialLinks.gitea && (
|
{socialLinks.gitea && (
|
||||||
<div class="tooltip" data-tip="Gitea">
|
<div class="tooltip" data-tip="Gitea">
|
||||||
<a
|
<a
|
||||||
href={socialLinks.gitea}
|
href={socialLinks.gitea}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="btn btn-circle btn-ghost"
|
class="btn btn-circle btn-ghost"
|
||||||
aria-label="Gitea"
|
aria-label="Gitea"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
role="img"
|
||||||
<path d="M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z"/>
|
viewBox="0 0 24 24"
|
||||||
</svg>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</a>
|
>
|
||||||
</div>
|
<path d="M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{socialLinks.linkedin && (
|
{socialLinks.linkedin && (
|
||||||
<div class="tooltip" data-tip="Linkedin">
|
<div class="tooltip" data-tip="Linkedin">
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ function isActive(href: string) {
|
|||||||
class="fixed top-0 left-0 right-0 z-[60] 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-2xl mx-auto"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="font-mono text-sm text-base-content/40 hover:text-primary transition-colors duration-200 tracking-tight"
|
class="font-mono text-sm text-base-content/40 hover:text-primary transition-colors duration-200 tracking-tight"
|
||||||
@@ -26,7 +28,8 @@ function isActive(href: string) {
|
|||||||
~/hadi
|
~/hadi
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div id="oneko-track" class="flex-1 relative h-12 pointer-events-none"></div>
|
<div id="oneko-track" class="flex-1 relative h-12 pointer-events-none">
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav class="hidden md:flex items-center">
|
<nav class="hidden md:flex items-center">
|
||||||
{
|
{
|
||||||
@@ -58,9 +61,15 @@ function isActive(href: string) {
|
|||||||
class="md:hidden flex flex-col gap-1 p-2 text-base-content/40 hover:text-base-content/70 transition-colors"
|
class="md:hidden flex flex-col gap-1 p-2 text-base-content/40 hover:text-base-content/70 transition-colors"
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
>
|
>
|
||||||
<span class="hamburger-line block w-4 h-px bg-current transition-all duration-200"></span>
|
<span
|
||||||
<span class="hamburger-line block w-4 h-px bg-current transition-all duration-200"></span>
|
class="hamburger-line block w-4 h-px bg-current transition-all duration-200"
|
||||||
<span class="hamburger-line block w-4 h-px bg-current transition-all duration-200"></span>
|
></span>
|
||||||
|
<span
|
||||||
|
class="hamburger-line block w-4 h-px bg-current transition-all duration-200"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="hamburger-line block w-4 h-px bg-current transition-all duration-200"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -113,7 +122,11 @@ function isActive(href: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
if (open && !btn.contains(e.target as Node) && !menu.contains(e.target as Node)) {
|
if (
|
||||||
|
open &&
|
||||||
|
!btn.contains(e.target as Node) &&
|
||||||
|
!menu.contains(e.target as Node)
|
||||||
|
) {
|
||||||
open = false;
|
open = false;
|
||||||
menu.style.display = "none";
|
menu.style.display = "none";
|
||||||
lines[0].style.transform = "";
|
lines[0].style.transform = "";
|
||||||
|
|||||||
@@ -27,10 +27,9 @@ function formatDate(date: Date) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate reading time (rough estimate based on word count)
|
|
||||||
const content = await Astro.slots.render("default");
|
const content = await Astro.slots.render("default");
|
||||||
const wordCount = content.split(/\s+/).length;
|
const wordCount = content.split(/\s+/).length;
|
||||||
const readingTime = Math.ceil(wordCount / 200); // Average reading speed: 200 words/min
|
const readingTime = Math.ceil(wordCount / 200);
|
||||||
|
|
||||||
const root = parse(content);
|
const root = parse(content);
|
||||||
const headers = root.querySelectorAll("h1, h2, h3");
|
const headers = root.querySelectorAll("h1, h2, h3");
|
||||||
@@ -38,14 +37,14 @@ const headers = root.querySelectorAll("h1, h2, h3");
|
|||||||
const toc = headers.map((header) => ({
|
const toc = headers.map((header) => ({
|
||||||
depth: parseInt(header.tagName.replace("H", "")),
|
depth: parseInt(header.tagName.replace("H", "")),
|
||||||
text: header.innerText.trim(),
|
text: header.innerText.trim(),
|
||||||
slug: header.getAttribute("id"), // Astro génère l'id automatiquement
|
slug: header.getAttribute("id"),
|
||||||
}));
|
}));
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={`${title} - Another Hadi`} description={description}>
|
<Layout title={`${title} - Another Hadi`} description={description}>
|
||||||
<article class="max-w-4xl mx-auto px-4 py-20">
|
<article class="max-w-4xl mx-auto px-4 py-20">
|
||||||
<BackToTop />
|
<BackToTop />
|
||||||
<!-- Back button -->
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<a href="/blog" class="btn btn-ghost btn-sm">
|
<a href="/blog" class="btn btn-ghost btn-sm">
|
||||||
<ChevronLeft size={18} />
|
<ChevronLeft size={18} />
|
||||||
@@ -53,22 +52,20 @@ const toc = headers.map((header) => ({
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Featured Image -->
|
|
||||||
{
|
{
|
||||||
image && (
|
image && (
|
||||||
<figure class="mb-8 rounded-2xl overflow-hidden">
|
<figure class="mb-8 rounded-2xl overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={image}
|
src={image}
|
||||||
alt={title}
|
alt={title}
|
||||||
class="w-full aspect-video object-cover"
|
class="w-full aspect-video object-cover"
|
||||||
width={1200}
|
width={1200}
|
||||||
height={630}
|
height={630}
|
||||||
/>
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Post Header -->
|
|
||||||
<header class="mb-8">
|
<header class="mb-8">
|
||||||
<h1 class="text-5xl font-bold mb-4">{title}</h1>
|
<h1 class="text-5xl font-bold mb-4">{title}</h1>
|
||||||
<p class="text-xl text-base-content/70 mb-4">{description}</p>
|
<p class="text-xl text-base-content/70 mb-4">{description}</p>
|
||||||
@@ -82,64 +79,58 @@ const toc = headers.map((header) => ({
|
|||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>{readingTime} min read</span>
|
<span>{readingTime} min read</span>
|
||||||
{
|
{
|
||||||
updatedDate && (
|
updatedDate && (
|
||||||
<>
|
<>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>Updated: {formatDate(updatedDate)}</span>
|
<span>Updated: {formatDate(updatedDate)}</span>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
tags && tags.length > 0 && (
|
tags && tags.length > 0 && (
|
||||||
<div class="flex flex-wrap gap-2 mb-4">
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<TagBadge tag={tag} />
|
<TagBadge tag={tag} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<Author />
|
<Author />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<!-- TOC -->
|
|
||||||
{
|
{
|
||||||
toc.length > 0 && (
|
toc.length > 0 && (
|
||||||
<div class="collapse bg-base-200/50 rounded-xl mb-8 border border-base-300">
|
<div class="collapse bg-base-200/50 rounded-xl mb-8 border border-base-300">
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
|
<p class="collapse-title font-bold uppercase text-xs tracking-widest opacity-60">
|
||||||
<p class="collapse-title font-bold uppercase text-xs tracking-widest opacity-60">
|
Table of Contents
|
||||||
Table of Contents
|
</p>
|
||||||
</p>
|
<div class="collapse-content text-sm">
|
||||||
<div class="collapse-content text-sm">
|
<ul class="space-y-3">
|
||||||
<ul class="space-y-3">
|
{toc.map((item) => (
|
||||||
{toc.map((item) => (
|
<li
|
||||||
<li
|
class={`list-none ${item.depth === 3 ? "ml-6 text-sm" : "font-medium"}`}
|
||||||
class={`list-none ${item.depth === 3 ? "ml-6 text-sm" : "font-medium"}`}
|
>
|
||||||
>
|
<a
|
||||||
<a
|
href={`#${item.slug}`}
|
||||||
href={`#${item.slug}`}
|
class="hover:link transition-all flex items-center gap-2"
|
||||||
class="hover:link transition-all flex items-center gap-2"
|
>
|
||||||
>
|
<span class="text-primary/40">{"#".repeat(item.depth)}</span>
|
||||||
<span class="text-primary/40">{"#".repeat(item.depth)}</span>
|
{item.text}
|
||||||
{item.text}
|
</a>
|
||||||
</a>
|
</li>
|
||||||
</li>
|
))}
|
||||||
))}
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
<nav class="bg-base-200/50 ">
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Post Content -->
|
|
||||||
<div
|
<div
|
||||||
class="max-w-none leading-7
|
class="max-w-none leading-7
|
||||||
[&_h1]:text-4xl [&_h1]:font-bold [&_h1]:mt-8 [&_h1]:mb-4
|
[&_h1]:text-4xl [&_h1]:font-bold [&_h1]:mt-8 [&_h1]:mb-4
|
||||||
@@ -163,17 +154,17 @@ const toc = headers.map((header) => ({
|
|||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<div class="divider mt-12"></div>
|
<div class="divider mt-12"></div>
|
||||||
|
|
||||||
<!-- Back to blog link -->
|
|
||||||
<div class="flex justify-center gap-2 mt-12">
|
<div class="flex justify-center gap-2 mt-12">
|
||||||
<div class="flex gap-3 justify-center flex-wrap text-sm">
|
<div class="flex gap-3 justify-center flex-wrap text-sm">
|
||||||
<a href="/blog" class="link link-hover">View All Posts</a>
|
<a href="/blog" class="link link-hover">View All Posts</a>
|
||||||
<span class="text-base-content/30">•</span>
|
<span class="text-base-content/30">•</span>
|
||||||
<a href="/#contact" class="link link-hover">Contact me</a>
|
<a href="/#contact" class="link link-hover">Contact me</a>
|
||||||
<span class="text-base-content/30">•</span>
|
<span class="text-base-content/30">•</span>
|
||||||
<a href="https://ko-fi.com/anotherhadi" class="link link-hover">Support me</a>
|
<a href="https://ko-fi.com/anotherhadi" class="link link-hover"
|
||||||
|
>Support me</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -78,8 +78,7 @@ const origin = Astro.url.origin;
|
|||||||
defer
|
defer
|
||||||
src="https://umami.hadi.icu/script.js"
|
src="https://umami.hadi.icu/script.js"
|
||||||
data-website-id="91b0c3a1-130a-4974-be47-078bc092cec8"
|
data-website-id="91b0c3a1-130a-4974-be47-078bc092cec8"
|
||||||
data-domains="hadi.icu,www.hadi.icu"
|
data-domains="hadi.icu,www.hadi.icu"></script>
|
||||||
></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen pt-12">
|
<body class="min-h-screen pt-12">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|||||||
@@ -124,9 +124,7 @@ const { title, description, image, tags, demoLink, url, sourceLink } =
|
|||||||
<!-- Back to projects link -->
|
<!-- Back to projects link -->
|
||||||
<div class="flex justify-center gap-2 mt-12">
|
<div class="flex justify-center gap-2 mt-12">
|
||||||
<div class="flex gap-3 justify-center flex-wrap text-sm">
|
<div class="flex gap-3 justify-center flex-wrap text-sm">
|
||||||
<a href="/projects" class="link link-hover"
|
<a href="/projects" class="link link-hover">View All Projects</a>
|
||||||
>View All Projects</a
|
|
||||||
>
|
|
||||||
<span class="text-base-content/30">•</span>
|
<span class="text-base-content/30">•</span>
|
||||||
<a href="/#contact" class="link link-hover">Contact me</a>
|
<a href="/#contact" class="link link-hover">Contact me</a>
|
||||||
<span class="text-base-content/30">•</span>
|
<span class="text-base-content/30">•</span>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const blogPosts = await getCollection("blog");
|
|||||||
const sortedPosts = blogPosts.sort(
|
const sortedPosts = blogPosts.sort(
|
||||||
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(),
|
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(),
|
||||||
);
|
);
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
|
|||||||
+332
-522
@@ -17,15 +17,20 @@ const { entry } = Astro.props;
|
|||||||
const { Content } = await render(entry);
|
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(getCategory))].sort();
|
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",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractLinks(body: string): string[] {
|
function extractLinks(body: string): string[] {
|
||||||
// Capture slug before optional #fragment: (/notes/slug) or (/notes/slug#section)
|
|
||||||
const re = /\(\/notes\/([^)#\s]+)(?:#[^)\s]*)?\)/g;
|
const re = /\(\/notes\/([^)#\s]+)(?:#[^)\s]*)?\)/g;
|
||||||
const ids: string[] = [];
|
const ids: string[] = [];
|
||||||
let m;
|
let m;
|
||||||
@@ -33,12 +38,14 @@ function extractLinks(body: string): string[] {
|
|||||||
return [...new Set(ids)];
|
return [...new Set(ids)];
|
||||||
}
|
}
|
||||||
|
|
||||||
const allLinks = Object.fromEntries(allNotes.map((n) => [n.id, extractLinks(n.body ?? "")]));
|
const allLinks = Object.fromEntries(
|
||||||
|
allNotes.map((n) => [n.id, extractLinks(n.body ?? "")]),
|
||||||
|
);
|
||||||
const forwardLinks = (allLinks[entry.id] ?? [])
|
const forwardLinks = (allLinks[entry.id] ?? [])
|
||||||
.map((id) => allNotes.find((n) => n.id === id))
|
.map((id) => allNotes.find((n) => n.id === id))
|
||||||
.filter(Boolean) as typeof allNotes;
|
.filter(Boolean) as typeof allNotes;
|
||||||
const backlinks = allNotes.filter(
|
const backlinks = allNotes.filter(
|
||||||
(n) => n.id !== entry.id && (allLinks[n.id] ?? []).includes(entry.id)
|
(n) => n.id !== entry.id && (allLinks[n.id] ?? []).includes(entry.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const graphNodes = [
|
const graphNodes = [
|
||||||
@@ -53,34 +60,32 @@ const graphEdges = [
|
|||||||
...backlinks.map((n) => ({ from: n.id, to: entry.id })),
|
...backlinks.map((n) => ({ from: n.id, to: entry.id })),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Mirrors github-slugger exactly: keeps _, keeps unicode letters/numbers, spaces → hyphens
|
// Mirrors github-slugger: keeps _, keeps unicode letters/numbers, spaces → hyphens
|
||||||
function slugify(text: string) {
|
function slugify(text: string) {
|
||||||
return text
|
return text
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^\p{L}\p{N}\s_-]/gu, "") // keep letters (unicode), numbers, spaces, _, -
|
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/ +/g, "-"); // spaces → hyphens (github-slugger does exactly this)
|
.replace(/ +/g, "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
const headings: { depth: number; text: string; id: string }[] = [];
|
const headings: { depth: number; text: string; id: string }[] = [];
|
||||||
const headingRe = /^(#{2,4}) (.+)$/gm;
|
const headingRe = /^(#{2,4}) (.+)$/gm;
|
||||||
let hm;
|
let hm;
|
||||||
while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
||||||
// Strip markdown formatting while preserving literal _ (word-internal underscores like my_var)
|
const raw = hm[2]
|
||||||
// Paired markers are stripped to their content; lone * are removed; _ only stripped at word boundaries
|
.trim()
|
||||||
const raw = hm[2].trim()
|
.replace(/`[^`]*`/g, "")
|
||||||
.replace(/`[^`]*`/g, "") // `code` → remove
|
.replace(/\*\*(.*?)\*\*/g, "$1")
|
||||||
.replace(/\*\*(.*?)\*\*/g, "$1") // **bold** → text
|
.replace(/(?<!\p{L}\p{N})__(.*?)__(?!\p{L}\p{N})/gu, "$1")
|
||||||
.replace(/(?<!\p{L}\p{N})__(.*?)__(?!\p{L}\p{N})/gu, "$1") // __bold__ → text
|
.replace(/\*(.*?)\*/g, "$1")
|
||||||
.replace(/\*(.*?)\*/g, "$1") // *italic* → text
|
.replace(/(?<!\p{L}\p{N})_(.*?)_(?!\p{L}\p{N})/gu, "$1")
|
||||||
.replace(/(?<!\p{L}\p{N})_(.*?)_(?!\p{L}\p{N})/gu, "$1") // _italic_ → text
|
.replace(/[*]/g, "");
|
||||||
.replace(/[*]/g, ""); // orphan * markers
|
|
||||||
headings.push({ depth: hm[1].length, text: raw, id: slugify(raw) });
|
headings.push({ depth: hm[1].length, text: raw, id: slugify(raw) });
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Both sidebars sit below the navbar when in drawer-open mode */
|
|
||||||
.drawer.lg\:drawer-open > .drawer-side,
|
.drawer.lg\:drawer-open > .drawer-side,
|
||||||
.drawer.xl\:drawer-open > .drawer-side {
|
.drawer.xl\:drawer-open > .drawer-side {
|
||||||
top: 3rem;
|
top: 3rem;
|
||||||
@@ -92,522 +97,327 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
|||||||
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">
|
||||||
|
<div class="drawer drawer-end xl:drawer-open min-h-[calc(100vh-3rem)]">
|
||||||
|
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
|
|
||||||
<div class="drawer drawer-end xl:drawer-open min-h-[calc(100vh-3rem)]">
|
<div class="drawer-content flex min-h-[calc(100vh-3rem)]">
|
||||||
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
|
<div class="drawer lg:drawer-open w-full">
|
||||||
|
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
|
|
||||||
<div class="drawer-content flex min-h-[calc(100vh-3rem)]">
|
<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="drawer lg:drawer-open w-full">
|
<div class="max-w-2xl mx-auto lg:mx-0">
|
||||||
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<a
|
||||||
<div class="drawer-content flex flex-col min-w-0">
|
href="/notes"
|
||||||
<main class="flex-1 px-4 sm:px-6 lg:px-10 py-6 lg:py-10 min-w-0">
|
class="inline-flex items-center gap-1 text-sm text-base-content/35 hover:text-base-content/70 transition-colors"
|
||||||
<div class="max-w-2xl mx-auto lg:mx-0">
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<a href="/notes" class="inline-flex items-center gap-1 text-sm text-base-content/35 hover:text-base-content/70 transition-colors">
|
|
||||||
<ChevronLeft size={14} />Notes
|
|
||||||
</a>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<label
|
|
||||||
for="nav-drawer"
|
|
||||||
class="lg:hidden flex items-center gap-1.5 font-mono text-xs text-base-content/40 hover:text-base-content/70 transition-colors border border-base-300/50 px-2 py-1 cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<List size={11} />
|
<ChevronLeft size={14} />Notes
|
||||||
nav
|
</a>
|
||||||
</label>
|
<div class="flex items-center gap-2">
|
||||||
<label
|
<label
|
||||||
for="graph-drawer"
|
for="nav-drawer"
|
||||||
id="graph-toggle"
|
class="lg:hidden flex items-center gap-1.5 font-mono text-xs text-base-content/40 hover:text-base-content/70 transition-colors border border-base-300/50 px-2 py-1 cursor-pointer"
|
||||||
class="flex items-center gap-1.5 font-mono text-xs text-base-content/40 hover:text-base-content/70 transition-colors border border-base-300/50 px-2 py-1 cursor-pointer"
|
>
|
||||||
title="Toggle graph"
|
<List size={11} />
|
||||||
>
|
nav
|
||||||
<PanelRight size={11} />
|
</label>
|
||||||
graph
|
<label
|
||||||
</label>
|
for="graph-drawer"
|
||||||
</div>
|
id="graph-toggle"
|
||||||
</div>
|
class="flex items-center gap-1.5 font-mono text-xs text-base-content/40 hover:text-base-content/70 transition-colors border border-base-300/50 px-2 py-1 cursor-pointer"
|
||||||
|
title="Toggle graph"
|
||||||
<header class="mb-8">
|
>
|
||||||
<div class="flex items-center gap-3 mb-5">
|
<PanelRight size={11} />
|
||||||
<span class="text-xl font-bold tracking-tight">
|
graph
|
||||||
<span class="text-primary/50 font-mono mr-0.5">/</span>{getCategory(entry)}
|
</label>
|
||||||
</span>
|
|
||||||
<span class="text-base-content/20 text-xs">·</span>
|
|
||||||
<time datetime={entry.data.publishDate.toISOString()} class="text-xs text-base-content/35">
|
|
||||||
{formatDate(entry.data.publishDate)}
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
{entry.data.tags.length > 0 && (
|
|
||||||
<div class="flex flex-wrap gap-1 mb-4">
|
|
||||||
{entry.data.tags.map((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">
|
|
||||||
{tag}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="border-t border-base-300/30 mb-6"></div>
|
<header class="mb-8">
|
||||||
|
<div class="flex items-center gap-3 mb-5">
|
||||||
{headings.length > 0 && (
|
<span class="text-xl font-bold tracking-tight">
|
||||||
<details class="mb-8 border border-base-300/40 group" style="background: oklch(4% 0 0);">
|
<span class="text-primary/50 font-mono mr-0.5">/</span>
|
||||||
<summary class="px-3 py-2 flex items-center gap-2 cursor-pointer list-none select-none font-mono text-xs text-base-content/35 hover:text-base-content/60 transition-colors">
|
{getCategory(entry)}
|
||||||
<span class="text-primary/40">§</span>
|
|
||||||
<span>table of contents</span>
|
|
||||||
<span class="ml-auto opacity-50 group-open:hidden">+</span>
|
|
||||||
<span class="ml-auto opacity-50 hidden group-open:inline">−</span>
|
|
||||||
</summary>
|
|
||||||
<nav class="px-3 pb-3 pt-1 border-t border-base-300/30 space-y-0.5">
|
|
||||||
{headings.map((h) => (
|
|
||||||
<a href={`#${h.id}`}
|
|
||||||
class:list={["block text-xs text-base-content/45 hover:text-base-content/80 transition-colors py-0.5", h.depth === 3 ? "pl-4" : h.depth === 4 ? "pl-8" : ""]}>
|
|
||||||
<span class="font-mono text-primary/25 mr-1.5">{"#".repeat(h.depth)}</span>{h.text}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</details>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="note-content text-sm leading-relaxed text-base-content/80
|
|
||||||
[&_h2]:text-lg [&_h2]:font-bold [&_h2]:mt-8 [&_h2]:mb-3 [&_h2]:text-base-content [&_h2]:tracking-tight [&_h2]:pb-1.5 [&_h2]:border-b [&_h2]:border-base-300/30
|
|
||||||
[&_h3]:text-base [&_h3]:font-semibold [&_h3]:mt-6 [&_h3]:mb-2 [&_h3]:text-base-content/90
|
|
||||||
[&_h4]:text-sm [&_h4]:font-semibold [&_h4]:mt-4 [&_h4]:mb-2 [&_h4]:text-base-content/80
|
|
||||||
[&_p]:mb-4 [&_p]:leading-relaxed
|
|
||||||
[&_a]:text-primary/80 [&_a]:underline [&_a]:underline-offset-2 [&_a]:hover:text-primary [&_a]:transition-colors
|
|
||||||
[&_ul]:mb-4 [&_ul]:ml-5 [&_ul]:list-none [&_ul]:space-y-1
|
|
||||||
[&_ul_li]:before:content-['–'] [&_ul_li]:before:text-base-content/25 [&_ul_li]:before:mr-2 [&_ul_li]:before:font-mono
|
|
||||||
[&_ol]:mb-4 [&_ol]:ml-5 [&_ol]:list-decimal [&_ol]:space-y-1
|
|
||||||
[&_li]:text-base-content/75
|
|
||||||
[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs [&_code]:bg-base-200 [&_code]:text-primary/80 [&_code]:border [&_code]:border-base-300/50
|
|
||||||
[&_pre]:p-4 [&_pre]:overflow-x-auto [&_pre]:mb-4 [&_pre]:bg-base-200/60 [&_pre]:border [&_pre]:border-base-300/50 [&_pre]:text-xs
|
|
||||||
[&_pre_code]:bg-transparent [&_pre_code]:border-0 [&_pre_code]:p-0 [&_pre_code]:text-base-content/80
|
|
||||||
[&_blockquote]:border-l-2 [&_blockquote]:border-primary/25 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:my-4 [&_blockquote]:text-base-content/50
|
|
||||||
[&_table]:w-full [&_table]:mb-6 [&_table]:text-xs [&_table]:border-collapse
|
|
||||||
[&_th]:text-left [&_th]:px-3 [&_th]:py-2 [&_th]:border [&_th]:border-base-300/50 [&_th]:bg-base-200/60 [&_th]:font-mono [&_th]:text-[10px] [&_th]:uppercase [&_th]:tracking-widest [&_th]:text-base-content/50
|
|
||||||
[&_td]:px-3 [&_td]:py-2 [&_td]:border [&_td]:border-base-300/40 [&_td]:font-mono [&_td]:text-xs [&_td]:text-base-content/70
|
|
||||||
[&_tr:nth-child(even)_td]:bg-base-200/20
|
|
||||||
[&_hr]:border-t [&_hr]:border-base-300/30 [&_hr]:my-8">
|
|
||||||
<Content />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
<aside
|
|
||||||
class="w-56 flex flex-col border-r border-base-300/60 h-full"
|
|
||||||
style="background: oklch(4% 0 0);"
|
|
||||||
>
|
|
||||||
<div class="px-4 py-4 border-b border-base-300/40">
|
|
||||||
<a href="/notes" class="flex items-center gap-2 mb-3 hover:text-primary transition-colors">
|
|
||||||
<Shield size={13} class="text-primary/60 shrink-0" />
|
|
||||||
<span class="font-mono text-xs text-primary/60 tracking-widest uppercase">security notes</span>
|
|
||||||
</a>
|
|
||||||
<div class="flex items-center gap-1.5 bg-base-200/50 px-2 py-1.5 border border-base-300/40">
|
|
||||||
<span class="font-mono text-xs text-base-content/30">›</span>
|
|
||||||
<input
|
|
||||||
data-search
|
|
||||||
type="text"
|
|
||||||
placeholder="search..."
|
|
||||||
class="bg-transparent font-mono text-xs text-base-content/70 placeholder:text-base-content/25 outline-none w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<nav class="px-3 py-3 flex-1 overflow-y-auto">
|
|
||||||
{categories.map((cat) => (
|
|
||||||
<div class="mb-4">
|
|
||||||
<div class="px-1 mb-1.5">
|
|
||||||
<span class="text-sm font-bold tracking-tight">
|
|
||||||
<span class="text-primary/50 font-mono mr-0.5">/</span>{cat}
|
|
||||||
</span>
|
</span>
|
||||||
|
<span class="text-base-content/20 text-xs">·</span>
|
||||||
|
<time
|
||||||
|
datetime={entry.data.publishDate.toISOString()}
|
||||||
|
class="text-xs text-base-content/35"
|
||||||
|
>
|
||||||
|
{formatDate(entry.data.publishDate)}
|
||||||
|
</time>
|
||||||
</div>
|
</div>
|
||||||
<ul class="ml-3 space-y-0.5 border-l border-base-300/30 pl-2">
|
<h1 class="text-4xl sm:text-5xl font-bold tracking-tight mb-3">
|
||||||
{sortedNotes.filter((n) => getCategory(n) === cat).map((n) => (
|
{entry.data.title}
|
||||||
<li>
|
</h1>
|
||||||
<a
|
<p class="text-base-content/50 mb-4">
|
||||||
href={`/notes/${n.id}`}
|
{entry.data.description}
|
||||||
class:list={[
|
</p>
|
||||||
"nav-item font-mono text-xs block py-0.5 px-1 truncate transition-colors",
|
{
|
||||||
n.id === entry.id
|
entry.data.tags.length > 0 && (
|
||||||
? "text-primary bg-primary/8"
|
<div class="flex flex-wrap gap-1 mb-4">
|
||||||
: "text-base-content/45 hover:text-base-content/80 hover:bg-base-200/30",
|
{entry.data.tags.map((tag) => (
|
||||||
]}
|
<a
|
||||||
data-title={n.data.title.toLowerCase()}
|
href={`/notes?tag=${tag}`}
|
||||||
data-tags={[...n.data.tags, ...extractInlineHashtags(n.body ?? "")].join(",")}
|
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"
|
||||||
>
|
>
|
||||||
{n.id === entry.id ? "▶ " : ""}{n.data.title}
|
{tag}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
))}
|
||||||
))}
|
</div>
|
||||||
</ul>
|
)
|
||||||
|
}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="border-t border-base-300/30 mb-6"></div>
|
||||||
|
|
||||||
|
{
|
||||||
|
headings.length > 0 && (
|
||||||
|
<details
|
||||||
|
class="mb-8 border border-base-300/40 group"
|
||||||
|
style="background: oklch(4% 0 0);"
|
||||||
|
>
|
||||||
|
<summary class="px-3 py-2 flex items-center gap-2 cursor-pointer list-none select-none font-mono text-xs text-base-content/35 hover:text-base-content/60 transition-colors">
|
||||||
|
<span class="text-primary/40">§</span>
|
||||||
|
<span>table of contents</span>
|
||||||
|
<span class="ml-auto opacity-50 group-open:hidden">+</span>
|
||||||
|
<span class="ml-auto opacity-50 hidden group-open:inline">−</span>
|
||||||
|
</summary>
|
||||||
|
<nav class="px-3 pb-3 pt-1 border-t border-base-300/30 space-y-0.5">
|
||||||
|
{headings.map((h) => (
|
||||||
|
<a
|
||||||
|
href={`#${h.id}`}
|
||||||
|
class:list={[
|
||||||
|
"block text-xs text-base-content/45 hover:text-base-content/80 transition-colors py-0.5",
|
||||||
|
h.depth === 3
|
||||||
|
? "pl-4"
|
||||||
|
: h.depth === 4
|
||||||
|
? "pl-8"
|
||||||
|
: "",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<span class="font-mono text-primary/25 mr-1.5">
|
||||||
|
{"#".repeat(h.depth)}
|
||||||
|
</span>
|
||||||
|
{h.text}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="note-content text-sm leading-relaxed text-base-content/80
|
||||||
|
[&_h2]:text-lg [&_h2]:font-bold [&_h2]:mt-8 [&_h2]:mb-3 [&_h2]:text-base-content [&_h2]:tracking-tight [&_h2]:pb-1.5 [&_h2]:border-b [&_h2]:border-base-300/30
|
||||||
|
[&_h3]:text-base [&_h3]:font-semibold [&_h3]:mt-6 [&_h3]:mb-2 [&_h3]:text-base-content/90
|
||||||
|
[&_h4]:text-sm [&_h4]:font-semibold [&_h4]:mt-4 [&_h4]:mb-2 [&_h4]:text-base-content/80
|
||||||
|
[&_p]:mb-4 [&_p]:leading-relaxed
|
||||||
|
[&_a]:text-primary/80 [&_a]:underline [&_a]:underline-offset-2 [&_a]:hover:text-primary [&_a]:transition-colors
|
||||||
|
[&_ul]:mb-4 [&_ul]:ml-5 [&_ul]:list-none [&_ul]:space-y-1
|
||||||
|
[&_ul_li]:before:content-['–'] [&_ul_li]:before:text-base-content/25 [&_ul_li]:before:mr-2 [&_ul_li]:before:font-mono
|
||||||
|
[&_ol]:mb-4 [&_ol]:ml-5 [&_ol]:list-decimal [&_ol]:space-y-1
|
||||||
|
[&_li]:text-base-content/75
|
||||||
|
[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs [&_code]:bg-base-200 [&_code]:text-primary/80 [&_code]:border [&_code]:border-base-300/50
|
||||||
|
[&_pre]:p-4 [&_pre]:overflow-x-auto [&_pre]:mb-4 [&_pre]:bg-base-200/60 [&_pre]:border [&_pre]:border-base-300/50 [&_pre]:text-xs
|
||||||
|
[&_pre_code]:bg-transparent [&_pre_code]:border-0 [&_pre_code]:p-0 [&_pre_code]:text-base-content/80
|
||||||
|
[&_blockquote]:border-l-2 [&_blockquote]:border-primary/25 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:my-4 [&_blockquote]:text-base-content/50
|
||||||
|
[&_table]:w-full [&_table]:mb-6 [&_table]:text-xs [&_table]:border-collapse
|
||||||
|
[&_th]:text-left [&_th]:px-3 [&_th]:py-2 [&_th]:border [&_th]:border-base-300/50 [&_th]:bg-base-200/60 [&_th]:font-mono [&_th]:text-[10px] [&_th]:uppercase [&_th]:tracking-widest [&_th]:text-base-content/50
|
||||||
|
[&_td]:px-3 [&_td]:py-2 [&_td]:border [&_td]:border-base-300/40 [&_td]:font-mono [&_td]:text-xs [&_td]:text-base-content/70
|
||||||
|
[&_tr:nth-child(even)_td]:bg-base-200/20
|
||||||
|
[&_hr]:border-t [&_hr]:border-base-300/30 [&_hr]:my-8"
|
||||||
|
>
|
||||||
|
<Content />
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</nav>
|
<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">
|
||||||
</aside>
|
<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>
|
||||||
|
<aside
|
||||||
|
class="w-56 flex flex-col border-r border-base-300/60 h-full"
|
||||||
|
style="background: oklch(4% 0 0);"
|
||||||
|
>
|
||||||
|
<div class="px-4 py-4 border-b border-base-300/40">
|
||||||
|
<a
|
||||||
|
href="/notes"
|
||||||
|
class="flex items-center gap-2 mb-3 hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<Shield size={13} class="text-primary/60 shrink-0" />
|
||||||
|
<span class="font-mono text-xs text-primary/60 tracking-widest uppercase">
|
||||||
|
security notes
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div class="flex items-center gap-1.5 bg-base-200/50 px-2 py-1.5 border border-base-300/40">
|
||||||
|
<span class="font-mono text-xs text-base-content/30">›</span>
|
||||||
|
<input
|
||||||
|
data-search
|
||||||
|
type="text"
|
||||||
|
placeholder="search..."
|
||||||
|
class="bg-transparent font-mono text-xs text-base-content/70 placeholder:text-base-content/25 outline-none w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="px-3 py-3 flex-1 overflow-y-auto">
|
||||||
|
{
|
||||||
|
categories.map((cat) => (
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="px-1 mb-1.5">
|
||||||
|
<span class="text-sm font-bold tracking-tight">
|
||||||
|
<span class="text-primary/50 font-mono mr-0.5">/</span>
|
||||||
|
{cat}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul class="ml-3 space-y-0.5 border-l border-base-300/30 pl-2">
|
||||||
|
{sortedNotes
|
||||||
|
.filter((n) => getCategory(n) === cat)
|
||||||
|
.map((n) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={`/notes/${n.id}`}
|
||||||
|
class:list={[
|
||||||
|
"nav-item font-mono text-xs block py-0.5 px-1 truncate transition-colors",
|
||||||
|
n.id === entry.id
|
||||||
|
? "text-primary bg-primary/8"
|
||||||
|
: "text-base-content/45 hover:text-base-content/80 hover:bg-base-200/30",
|
||||||
|
]}
|
||||||
|
data-title={n.data.title.toLowerCase()}
|
||||||
|
data-tags={[
|
||||||
|
...n.data.tags,
|
||||||
|
...extractInlineHashtags(n.body ?? ""),
|
||||||
|
].join(",")}
|
||||||
|
>
|
||||||
|
{n.id === entry.id ? "▶ " : ""}
|
||||||
|
{n.data.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-side z-40">
|
||||||
|
<label
|
||||||
|
for="graph-drawer"
|
||||||
|
aria-label="close sidebar"
|
||||||
|
class="drawer-overlay xl:hidden"
|
||||||
|
></label>
|
||||||
|
<aside
|
||||||
|
id="right-sidebar"
|
||||||
|
class="w-52 flex flex-col border-l border-base-300/60 h-full overflow-y-auto"
|
||||||
|
style="background: oklch(4% 0 0);"
|
||||||
|
>
|
||||||
|
<div class="border-b border-base-300/40">
|
||||||
|
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest px-3 pt-3 pb-2">
|
||||||
|
graph
|
||||||
|
</p>
|
||||||
|
<canvas
|
||||||
|
id="note-graph"
|
||||||
|
height="190"
|
||||||
|
role="img"
|
||||||
|
aria-label="Graph of linked notes"
|
||||||
|
style="width:100%; display:block; background: oklch(2% 0 0); cursor:default;"
|
||||||
|
></canvas>
|
||||||
|
{
|
||||||
|
graphNodes.length < 2 && (
|
||||||
|
<p class="font-mono text-[9px] text-base-content/20 text-center py-2">
|
||||||
|
no connections yet
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
forwardLinks.length > 0 && (
|
||||||
|
<div class="p-3 border-b border-base-300/40">
|
||||||
|
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
|
||||||
|
links
|
||||||
|
</p>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
{forwardLinks.map((n) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={`/notes/${n.id}`}
|
||||||
|
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
|
||||||
|
>
|
||||||
|
→ {n.data.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
backlinks.length > 0 && (
|
||||||
|
<div class="p-3">
|
||||||
|
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
|
||||||
|
backlinks
|
||||||
|
</p>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
{backlinks.map((n) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={`/notes/${n.id}`}
|
||||||
|
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
|
||||||
|
>
|
||||||
|
← {n.data.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
forwardLinks.length === 0 && backlinks.length === 0 && (
|
||||||
|
<div class="p-3">
|
||||||
|
<p class="font-mono text-[9px] text-base-content/20">
|
||||||
|
no linked notes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="px-4 py-6">
|
||||||
|
<Author />
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drawer-side z-40">
|
<script is:inline define:vars={{ graphNodes, graphEdges }}>
|
||||||
<label for="graph-drawer" aria-label="close sidebar" class="drawer-overlay xl:hidden"></label>
|
window.__graphNodes = graphNodes;
|
||||||
<aside
|
window.__graphEdges = graphEdges;
|
||||||
id="right-sidebar"
|
</script>
|
||||||
class="w-52 flex flex-col border-l border-base-300/60 h-full overflow-y-auto"
|
|
||||||
style="background: oklch(4% 0 0);"
|
|
||||||
>
|
|
||||||
<div class="border-b border-base-300/40">
|
|
||||||
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest px-3 pt-3 pb-2">graph</p>
|
|
||||||
<canvas
|
|
||||||
id="note-graph"
|
|
||||||
height="190"
|
|
||||||
role="img"
|
|
||||||
aria-label="Graph of linked notes"
|
|
||||||
style="width:100%; display:block; background: oklch(2% 0 0); cursor:default;"
|
|
||||||
></canvas>
|
|
||||||
{graphNodes.length <= 1 && (
|
|
||||||
<p class="font-mono text-[9px] text-base-content/20 text-center py-2">no connections yet</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{forwardLinks.length > 0 && (
|
<script>
|
||||||
<div class="p-3 border-b border-base-300/40">
|
import "../../utils/notes-graph.ts";
|
||||||
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">links</p>
|
</script>
|
||||||
<ul class="space-y-1">
|
</main>
|
||||||
{forwardLinks.map((n) => (
|
|
||||||
<li>
|
|
||||||
<a href={`/notes/${n.id}`}
|
|
||||||
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate">
|
|
||||||
→ {n.data.title}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{backlinks.length > 0 && (
|
|
||||||
<div class="p-3">
|
|
||||||
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">backlinks</p>
|
|
||||||
<ul class="space-y-1">
|
|
||||||
{backlinks.map((n) => (
|
|
||||||
<li>
|
|
||||||
<a href={`/notes/${n.id}`}
|
|
||||||
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate">
|
|
||||||
← {n.data.title}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{forwardLinks.length === 0 && backlinks.length === 0 && (
|
|
||||||
<div class="p-3">
|
|
||||||
<p class="font-mono text-[9px] text-base-content/20">no linked notes</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="px-4 py-6">
|
|
||||||
<Author />
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script is:inline define:vars={{ graphNodes, graphEdges }}>
|
|
||||||
window.__graphNodes = graphNodes;
|
|
||||||
window.__graphEdges = graphEdges;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const PRIMARY = "oklch(71% 0.0863 296.59)";
|
|
||||||
|
|
||||||
type GNode = { id: string; title: string; current: boolean; x: number; y: number; vx: number; vy: number };
|
|
||||||
type GEdge = { from: string; to: string };
|
|
||||||
let stopGraph: (() => void) | null = null;
|
|
||||||
|
|
||||||
function startGraph(): (() => void) | null {
|
|
||||||
const w = window as typeof window & { __graphNodes?: { id: string; title: string; current: boolean }[]; __graphEdges?: GEdge[] };
|
|
||||||
const graphNodes = w.__graphNodes ?? [];
|
|
||||||
const graphEdges: GEdge[] = w.__graphEdges ?? [];
|
|
||||||
const canvas = document.getElementById("note-graph") as HTMLCanvasElement | null;
|
|
||||||
if (!canvas || graphNodes.length === 0) return null;
|
|
||||||
|
|
||||||
const W = canvas.width = canvas.offsetWidth;
|
|
||||||
const H = canvas.height = 190;
|
|
||||||
const ctx = canvas.getContext("2d")!;
|
|
||||||
|
|
||||||
const nodes: GNode[] = graphNodes.map((n) => ({
|
|
||||||
...n,
|
|
||||||
x: n.current ? W / 2 : W / 2 + (Math.random() - 0.5) * 80,
|
|
||||||
y: n.current ? H / 2 : H / 2 + (Math.random() - 0.5) * 80,
|
|
||||||
vx: 0,
|
|
||||||
vy: 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
let dragging: GNode | null = null;
|
|
||||||
let hovered: GNode | null = null;
|
|
||||||
|
|
||||||
function nodeAt(x: number, y: number): GNode | null {
|
|
||||||
return nodes.find((n) => {
|
|
||||||
const dx = n.x - x, dy = n.y - y;
|
|
||||||
return Math.sqrt(dx * dx + dy * dy) < (n.current ? 10 : 8);
|
|
||||||
}) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tick() {
|
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
|
||||||
for (let j = i + 1; j < nodes.length; j++) {
|
|
||||||
const a = nodes[i], b = nodes[j];
|
|
||||||
const dx = b.x - a.x, dy = b.y - a.y;
|
|
||||||
const d = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
||||||
const f = 900 / (d * d);
|
|
||||||
a.vx -= (dx / d) * f; a.vy -= (dy / d) * f;
|
|
||||||
b.vx += (dx / d) * f; b.vy += (dy / d) * f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const e of graphEdges) {
|
|
||||||
const a = nodes.find((n: GNode) => n.id === e.from);
|
|
||||||
const b = nodes.find((n: GNode) => n.id === e.to);
|
|
||||||
if (!a || !b) continue;
|
|
||||||
const dx = b.x - a.x, dy = b.y - a.y;
|
|
||||||
const d = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
||||||
const f = (d - 75) * 0.04;
|
|
||||||
a.vx += (dx / d) * f; a.vy += (dy / d) * f;
|
|
||||||
b.vx -= (dx / d) * f; b.vy -= (dy / d) * f;
|
|
||||||
}
|
|
||||||
for (const n of nodes) {
|
|
||||||
n.vx += (W / 2 - n.x) * 0.025;
|
|
||||||
n.vy += (H / 2 - n.y) * 0.025;
|
|
||||||
}
|
|
||||||
for (const n of nodes) {
|
|
||||||
if (n === dragging) continue;
|
|
||||||
n.vx *= 0.78; n.vy *= 0.78;
|
|
||||||
n.x = Math.max(16, Math.min(W - 16, n.x + n.vx));
|
|
||||||
n.y = Math.max(16, Math.min(H - 16, n.y + n.vy));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
ctx.clearRect(0, 0, W, H);
|
|
||||||
ctx.fillStyle = "oklch(2% 0 0)";
|
|
||||||
ctx.fillRect(0, 0, W, H);
|
|
||||||
|
|
||||||
const connected = new Set();
|
|
||||||
if (hovered) {
|
|
||||||
for (const e of graphEdges) {
|
|
||||||
if (e.from === hovered.id) connected.add(e.to);
|
|
||||||
if (e.to === hovered.id) connected.add(e.from);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const e of graphEdges) {
|
|
||||||
const a = nodes.find((n: GNode) => n.id === e.from);
|
|
||||||
const b = nodes.find((n: GNode) => n.id === e.to);
|
|
||||||
if (!a || !b) continue;
|
|
||||||
const lit = hovered && (e.from === hovered.id || e.to === hovered.id);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(a.x, a.y);
|
|
||||||
ctx.lineTo(b.x, b.y);
|
|
||||||
ctx.strokeStyle = lit ? "oklch(55% 0 0)" : "oklch(27% 0 0)";
|
|
||||||
ctx.lineWidth = lit ? 1.5 : 1;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const n of nodes) {
|
|
||||||
const isHov = hovered?.id === n.id;
|
|
||||||
const isCon = connected.has(n.id);
|
|
||||||
const r = n.current ? 7 : isHov ? 6 : 4.5;
|
|
||||||
|
|
||||||
if (isHov && !n.current) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(n.x, n.y, r + 5, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = "oklch(71% 0.0863 296.59 / 0.15)";
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = n.current
|
|
||||||
? PRIMARY
|
|
||||||
: isHov ? "oklch(78% 0.05 296.59)"
|
|
||||||
: isCon ? "oklch(58% 0.03 296.59)"
|
|
||||||
: "oklch(40% 0 0)";
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
if (n.current || isHov || isCon) {
|
|
||||||
ctx.font = `${n.current ? "10px" : "9px"} monospace`;
|
|
||||||
ctx.textAlign = "center";
|
|
||||||
ctx.fillStyle = n.current ? "oklch(87% 0 0)" : "oklch(62% 0 0)";
|
|
||||||
const label = n.title.length > 14 ? n.title.slice(0, 13) + "…" : n.title;
|
|
||||||
ctx.fillText(label, n.x, n.y + r + 9);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let animId: number;
|
|
||||||
function loop() { tick(); draw(); animId = requestAnimationFrame(loop); }
|
|
||||||
animId = requestAnimationFrame(loop);
|
|
||||||
|
|
||||||
canvas.addEventListener("mousedown", (e) => {
|
|
||||||
const r = canvas.getBoundingClientRect();
|
|
||||||
const sx = W / canvas.offsetWidth;
|
|
||||||
dragging = nodeAt((e.clientX - r.left) * sx, (e.clientY - r.top) * (H / canvas.offsetHeight));
|
|
||||||
});
|
|
||||||
canvas.addEventListener("mousemove", (e) => {
|
|
||||||
const r = canvas.getBoundingClientRect();
|
|
||||||
const sx = W / canvas.offsetWidth;
|
|
||||||
const x = (e.clientX - r.left) * sx;
|
|
||||||
const y = (e.clientY - r.top) * (H / canvas.offsetHeight);
|
|
||||||
if (dragging) { dragging.x = x; dragging.y = y; dragging.vx = 0; dragging.vy = 0; }
|
|
||||||
hovered = nodeAt(x, y);
|
|
||||||
canvas.style.cursor = hovered && !hovered.current ? "pointer" : "default";
|
|
||||||
});
|
|
||||||
canvas.addEventListener("mouseup", () => { dragging = null; });
|
|
||||||
canvas.addEventListener("mouseleave", () => { dragging = null; hovered = null; });
|
|
||||||
canvas.addEventListener("click", (e) => {
|
|
||||||
const r = canvas.getBoundingClientRect();
|
|
||||||
const sx = W / canvas.offsetWidth;
|
|
||||||
const n = nodeAt((e.clientX - r.left) * sx, (e.clientY - r.top) * (H / canvas.offsetHeight));
|
|
||||||
if (n && !n.current) window.location.href = `/notes/${n.id}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => cancelAnimationFrame(animId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
if (stopGraph) { stopGraph(); stopGraph = null; }
|
|
||||||
|
|
||||||
const graphDrawer = document.getElementById("graph-drawer") as HTMLInputElement | null;
|
|
||||||
if (!graphDrawer) return;
|
|
||||||
|
|
||||||
// On non-xl: let DaisyUI overlay work via checkbox
|
|
||||||
function onGraphDrawerChange() {
|
|
||||||
if (graphDrawer!.checked) {
|
|
||||||
requestAnimationFrame(() => { stopGraph = startGraph() ?? null; });
|
|
||||||
} else {
|
|
||||||
if (stopGraph) { stopGraph(); stopGraph = null; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
graphDrawer.addEventListener("change", onGraphDrawerChange);
|
|
||||||
|
|
||||||
const outerDrawer = graphDrawer.closest<HTMLElement>(".drawer.drawer-end");
|
|
||||||
const xlQuery = window.matchMedia("(min-width: 1280px)");
|
|
||||||
|
|
||||||
function setXlSidebar(open: boolean) {
|
|
||||||
if (!outerDrawer) return;
|
|
||||||
if (open) {
|
|
||||||
outerDrawer.classList.add("xl:drawer-open");
|
|
||||||
requestAnimationFrame(() => { stopGraph = startGraph() ?? null; });
|
|
||||||
} else {
|
|
||||||
outerDrawer.classList.remove("xl:drawer-open");
|
|
||||||
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")) {
|
|
||||||
const s = document.createElement("style");
|
|
||||||
s.id = "heading-anchor-styles";
|
|
||||||
s.textContent = `
|
|
||||||
.note-content h2, .note-content h3, .note-content h4 {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
.heading-anchor {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-left: 0.4em;
|
|
||||||
color: oklch(38% 0 0);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 120ms, color 120ms;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.note-content h2:hover .heading-anchor,
|
|
||||||
.note-content h3:hover .heading-anchor,
|
|
||||||
.note-content h4:hover .heading-anchor { opacity: 1; }
|
|
||||||
.heading-anchor:hover, .heading-anchor.copied { color: oklch(71% 0.0863 296.59); opacity: 1; }
|
|
||||||
`;
|
|
||||||
document.head.appendChild(s);
|
|
||||||
}
|
|
||||||
document.querySelectorAll(".note-content h2, .note-content h3, .note-content h4").forEach((heading) => {
|
|
||||||
if (!heading.id || heading.querySelector(".heading-anchor")) return;
|
|
||||||
const anchor = document.createElement("a");
|
|
||||||
anchor.href = `#${heading.id}`;
|
|
||||||
anchor.className = "heading-anchor";
|
|
||||||
anchor.setAttribute("aria-label", "Copy link to section");
|
|
||||||
anchor.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
|
|
||||||
anchor.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const url = `${location.origin}${location.pathname}#${heading.id}`;
|
|
||||||
navigator.clipboard.writeText(url).then(() => {
|
|
||||||
anchor.classList.add("copied");
|
|
||||||
setTimeout(() => anchor.classList.remove("copied"), 1800);
|
|
||||||
});
|
|
||||||
history.pushState(null, "", `#${heading.id}`);
|
|
||||||
});
|
|
||||||
heading.appendChild(anchor);
|
|
||||||
});
|
|
||||||
|
|
||||||
const navItems = document.querySelectorAll<HTMLElement>(".nav-item");
|
|
||||||
document.querySelectorAll<HTMLInputElement>("[data-search]").forEach((input) => {
|
|
||||||
input.addEventListener("input", (e) => {
|
|
||||||
const target = e.target as HTMLInputElement;
|
|
||||||
const raw = target.value.toLowerCase().trim();
|
|
||||||
document.querySelectorAll<HTMLInputElement>("[data-search]").forEach((o) => {
|
|
||||||
if (o !== target) o.value = target.value;
|
|
||||||
});
|
|
||||||
const isTag = raw.startsWith("#");
|
|
||||||
const search = isTag ? raw.slice(1) : raw;
|
|
||||||
navItems.forEach((item) => {
|
|
||||||
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";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("astro:page-load", init);
|
|
||||||
document.addEventListener("astro:before-preparation", () => {
|
|
||||||
if (stopGraph) { stopGraph(); stopGraph = null; }
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
+34
-22
@@ -6,7 +6,7 @@ import { getCategory } from "../../utils/notes";
|
|||||||
|
|
||||||
const notes = await getCollection("notes");
|
const notes = await getCollection("notes");
|
||||||
const sortedNotes = notes.sort(
|
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(getCategory))].sort();
|
const categories = [...new Set(notes.map(getCategory))].sort();
|
||||||
@@ -15,7 +15,7 @@ const searchIndex = Object.fromEntries(
|
|||||||
sortedNotes.map((n) => [
|
sortedNotes.map((n) => [
|
||||||
n.id,
|
n.id,
|
||||||
[n.data.title, n.data.description, n.body ?? ""].join(" ").toLowerCase(),
|
[n.data.title, n.data.description, n.body ?? ""].join(" ").toLowerCase(),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -24,11 +24,13 @@ const searchIndex = Object.fromEntries(
|
|||||||
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">
|
||||||
|
<div class="text-center mb-12">
|
||||||
<div class="text-center mb-12">
|
|
||||||
<div class="flex items-center justify-center gap-2 mb-4">
|
<div class="flex items-center justify-center gap-2 mb-4">
|
||||||
<Shield size={20} class="text-primary/60" />
|
<Shield size={20} class="text-primary/60" />
|
||||||
<span class="font-mono text-xs text-primary/60 tracking-widest uppercase">security notes</span>
|
<span
|
||||||
|
class="font-mono text-xs text-primary/60 tracking-widest uppercase"
|
||||||
|
>security notes</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-4xl sm:text-5xl font-bold mb-4">Notes</h1>
|
<h1 class="text-4xl sm:text-5xl font-bold mb-4">Notes</h1>
|
||||||
<p class="text-base-content/50 max-w-md mx-auto">
|
<p class="text-base-content/50 max-w-md mx-auto">
|
||||||
@@ -36,8 +38,10 @@ const searchIndex = Object.fromEntries(
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-12 max-w-sm mx-auto">
|
<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">
|
<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>
|
<span class="font-mono text-sm text-base-content/25">›</span>
|
||||||
<input
|
<input
|
||||||
data-search
|
data-search
|
||||||
@@ -51,15 +55,16 @@ const searchIndex = Object.fromEntries(
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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) => getCategory(n) === 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">
|
||||||
<h2 class="text-xl font-bold tracking-tight">
|
<h2 class="text-xl font-bold tracking-tight">
|
||||||
<span class="text-primary/50 font-mono mr-1">/</span>{cat}
|
<span class="text-primary/50 font-mono mr-1">/</span>
|
||||||
|
{cat}
|
||||||
</h2>
|
</h2>
|
||||||
<span class="font-mono text-xs text-base-content/25">
|
<span class="font-mono text-xs text-base-content/25">
|
||||||
{catNotes.length} note{catNotes.length !== 1 ? "s" : ""}
|
{catNotes.length} note{catNotes.length !== 1 ? "s" : ""}
|
||||||
@@ -67,7 +72,7 @@ const searchIndex = Object.fromEntries(
|
|||||||
</div>
|
</div>
|
||||||
<div class="border-t border-base-300/40 mb-1" />
|
<div class="border-t border-base-300/40 mb-1" />
|
||||||
|
|
||||||
<ul class="divide-y divide-base-300/20">
|
<ul class="divide-y divide-base-300/20">
|
||||||
{catNotes.map((n) => (
|
{catNotes.map((n) => (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
@@ -109,14 +114,18 @@ const searchIndex = Object.fromEntries(
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="empty-state" class="hidden text-center py-20 font-mono text-sm text-base-content/25">
|
<div
|
||||||
|
id="empty-state"
|
||||||
|
class="hidden text-center py-20 font-mono text-sm text-base-content/25"
|
||||||
|
>
|
||||||
no results.
|
no results.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-center font-mono text-xs text-base-content/20 mt-16">
|
<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
|
<span id="note-count">{notes.length}</span> note{
|
||||||
|
notes.length !== 1 ? "s" : ""
|
||||||
|
} total
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script is:inline define:vars={{ searchIndex }}>
|
<script is:inline define:vars={{ searchIndex }}>
|
||||||
@@ -135,18 +144,19 @@ const searchIndex = Object.fromEntries(
|
|||||||
noteCards.forEach((card) => {
|
noteCards.forEach((card) => {
|
||||||
const id = card.dataset.id ?? "";
|
const id = card.dataset.id ?? "";
|
||||||
const tags = card.dataset.tags ? card.dataset.tags.split(",") : [];
|
const tags = card.dataset.tags ? card.dataset.tags.split(",") : [];
|
||||||
const show = !query || (
|
const show =
|
||||||
isTag
|
!query ||
|
||||||
? tags.some((t) => t.includes(query)) || (searchIndex[id] ?? "").includes(`#${query}`)
|
(isTag
|
||||||
: (searchIndex[id] ?? "").includes(query)
|
? tags.some((t) => t.includes(query)) ||
|
||||||
);
|
(searchIndex[id] ?? "").includes(`#${query}`)
|
||||||
|
: (searchIndex[id] ?? "").includes(query));
|
||||||
card.style.display = show ? "" : "none";
|
card.style.display = show ? "" : "none";
|
||||||
if (show) visible++;
|
if (show) visible++;
|
||||||
});
|
});
|
||||||
|
|
||||||
sections.forEach((section) => {
|
sections.forEach((section) => {
|
||||||
const anyVisible = [...section.querySelectorAll(".note-card")].some(
|
const anyVisible = [...section.querySelectorAll(".note-card")].some(
|
||||||
(c) => c.style.display !== "none"
|
(c) => c.style.display !== "none",
|
||||||
);
|
);
|
||||||
section.style.display = anyVisible ? "" : "none";
|
section.style.display = anyVisible ? "" : "none";
|
||||||
});
|
});
|
||||||
@@ -164,7 +174,9 @@ const searchIndex = Object.fromEntries(
|
|||||||
|
|
||||||
const urlTag = new URLSearchParams(window.location.search).get("tag");
|
const urlTag = new URLSearchParams(window.location.search).get("tag");
|
||||||
if (urlTag) {
|
if (urlTag) {
|
||||||
document.querySelectorAll("[data-search]").forEach((i) => { i.value = `#${urlTag}`; });
|
document.querySelectorAll("[data-search]").forEach((i) => {
|
||||||
|
i.value = `#${urlTag}`;
|
||||||
|
});
|
||||||
filter(`#${urlTag}`);
|
filter(`#${urlTag}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,356 @@
|
|||||||
|
const PRIMARY = "oklch(71% 0.0863 296.59)";
|
||||||
|
|
||||||
|
type GNode = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
current: boolean;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
vx: number;
|
||||||
|
vy: number;
|
||||||
|
};
|
||||||
|
type GEdge = { from: string; to: string };
|
||||||
|
|
||||||
|
let stopGraph: (() => void) | null = null;
|
||||||
|
|
||||||
|
function startGraph(): (() => void) | null {
|
||||||
|
const w = window as typeof window & {
|
||||||
|
__graphNodes?: { id: string; title: string; current: boolean }[];
|
||||||
|
__graphEdges?: GEdge[];
|
||||||
|
};
|
||||||
|
const graphNodes = w.__graphNodes ?? [];
|
||||||
|
const graphEdges: GEdge[] = w.__graphEdges ?? [];
|
||||||
|
const canvas = document.getElementById(
|
||||||
|
"note-graph",
|
||||||
|
) as HTMLCanvasElement | null;
|
||||||
|
if (!canvas || graphNodes.length === 0) return null;
|
||||||
|
|
||||||
|
const W = (canvas.width = canvas.offsetWidth);
|
||||||
|
const H = (canvas.height = 190);
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
|
||||||
|
const nodes: GNode[] = graphNodes.map((n) => ({
|
||||||
|
...n,
|
||||||
|
x: n.current ? W / 2 : W / 2 + (Math.random() - 0.5) * 80,
|
||||||
|
y: n.current ? H / 2 : H / 2 + (Math.random() - 0.5) * 80,
|
||||||
|
vx: 0,
|
||||||
|
vy: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let dragging: GNode | null = null;
|
||||||
|
let hovered: GNode | null = null;
|
||||||
|
|
||||||
|
function nodeAt(x: number, y: number): GNode | null {
|
||||||
|
return (
|
||||||
|
nodes.find((n) => {
|
||||||
|
const dx = n.x - x,
|
||||||
|
dy = n.y - y;
|
||||||
|
return Math.sqrt(dx * dx + dy * dy) < (n.current ? 10 : 8);
|
||||||
|
}) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
for (let j = i + 1; j < nodes.length; j++) {
|
||||||
|
const a = nodes[i],
|
||||||
|
b = nodes[j];
|
||||||
|
const dx = b.x - a.x,
|
||||||
|
dy = b.y - a.y;
|
||||||
|
const d = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||||
|
const f = 900 / (d * d);
|
||||||
|
a.vx -= (dx / d) * f;
|
||||||
|
a.vy -= (dy / d) * f;
|
||||||
|
b.vx += (dx / d) * f;
|
||||||
|
b.vy += (dy / d) * f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const e of graphEdges) {
|
||||||
|
const a = nodes.find((n: GNode) => n.id === e.from);
|
||||||
|
const b = nodes.find((n: GNode) => n.id === e.to);
|
||||||
|
if (!a || !b) continue;
|
||||||
|
const dx = b.x - a.x,
|
||||||
|
dy = b.y - a.y;
|
||||||
|
const d = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||||
|
const f = (d - 75) * 0.04;
|
||||||
|
a.vx += (dx / d) * f;
|
||||||
|
a.vy += (dy / d) * f;
|
||||||
|
b.vx -= (dx / d) * f;
|
||||||
|
b.vy -= (dy / d) * f;
|
||||||
|
}
|
||||||
|
for (const n of nodes) {
|
||||||
|
n.vx += (W / 2 - n.x) * 0.025;
|
||||||
|
n.vy += (H / 2 - n.y) * 0.025;
|
||||||
|
}
|
||||||
|
for (const n of nodes) {
|
||||||
|
if (n === dragging) continue;
|
||||||
|
n.vx *= 0.78;
|
||||||
|
n.vy *= 0.78;
|
||||||
|
n.x = Math.max(16, Math.min(W - 16, n.x + n.vx));
|
||||||
|
n.y = Math.max(16, Math.min(H - 16, n.y + n.vy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
ctx.clearRect(0, 0, W, H);
|
||||||
|
ctx.fillStyle = "oklch(2% 0 0)";
|
||||||
|
ctx.fillRect(0, 0, W, H);
|
||||||
|
|
||||||
|
const connected = new Set<string>();
|
||||||
|
if (hovered) {
|
||||||
|
for (const e of graphEdges) {
|
||||||
|
if (e.from === hovered.id) connected.add(e.to);
|
||||||
|
if (e.to === hovered.id) connected.add(e.from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const e of graphEdges) {
|
||||||
|
const a = nodes.find((n: GNode) => n.id === e.from);
|
||||||
|
const b = nodes.find((n: GNode) => n.id === e.to);
|
||||||
|
if (!a || !b) continue;
|
||||||
|
const lit = hovered && (e.from === hovered.id || e.to === hovered.id);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(a.x, a.y);
|
||||||
|
ctx.lineTo(b.x, b.y);
|
||||||
|
ctx.strokeStyle = lit ? "oklch(55% 0 0)" : "oklch(27% 0 0)";
|
||||||
|
ctx.lineWidth = lit ? 1.5 : 1;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const n of nodes) {
|
||||||
|
const isHov = hovered?.id === n.id;
|
||||||
|
const isCon = connected.has(n.id);
|
||||||
|
const r = n.current ? 7 : isHov ? 6 : 4.5;
|
||||||
|
|
||||||
|
if (isHov && !n.current) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(n.x, n.y, r + 5, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = "oklch(71% 0.0863 296.59 / 0.15)";
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = n.current
|
||||||
|
? PRIMARY
|
||||||
|
: isHov
|
||||||
|
? "oklch(78% 0.05 296.59)"
|
||||||
|
: isCon
|
||||||
|
? "oklch(58% 0.03 296.59)"
|
||||||
|
: "oklch(40% 0 0)";
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
if (n.current || isHov || isCon) {
|
||||||
|
ctx.font = `${n.current ? "10px" : "9px"} monospace`;
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.fillStyle = n.current ? "oklch(87% 0 0)" : "oklch(62% 0 0)";
|
||||||
|
const label =
|
||||||
|
n.title.length > 14 ? n.title.slice(0, 13) + "…" : n.title;
|
||||||
|
ctx.fillText(label, n.x, n.y + r + 9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let animId: number;
|
||||||
|
function loop() {
|
||||||
|
tick();
|
||||||
|
draw();
|
||||||
|
animId = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
animId = requestAnimationFrame(loop);
|
||||||
|
|
||||||
|
canvas.addEventListener("mousedown", (e) => {
|
||||||
|
const r = canvas.getBoundingClientRect();
|
||||||
|
const sx = W / canvas.offsetWidth;
|
||||||
|
dragging = nodeAt(
|
||||||
|
(e.clientX - r.left) * sx,
|
||||||
|
(e.clientY - r.top) * (H / canvas.offsetHeight),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
canvas.addEventListener("mousemove", (e) => {
|
||||||
|
const r = canvas.getBoundingClientRect();
|
||||||
|
const sx = W / canvas.offsetWidth;
|
||||||
|
const x = (e.clientX - r.left) * sx;
|
||||||
|
const y = (e.clientY - r.top) * (H / canvas.offsetHeight);
|
||||||
|
if (dragging) {
|
||||||
|
dragging.x = x;
|
||||||
|
dragging.y = y;
|
||||||
|
dragging.vx = 0;
|
||||||
|
dragging.vy = 0;
|
||||||
|
}
|
||||||
|
hovered = nodeAt(x, y);
|
||||||
|
canvas.style.cursor =
|
||||||
|
hovered && !hovered.current ? "pointer" : "default";
|
||||||
|
});
|
||||||
|
canvas.addEventListener("mouseup", () => {
|
||||||
|
dragging = null;
|
||||||
|
});
|
||||||
|
canvas.addEventListener("mouseleave", () => {
|
||||||
|
dragging = null;
|
||||||
|
hovered = null;
|
||||||
|
});
|
||||||
|
canvas.addEventListener("click", (e) => {
|
||||||
|
const r = canvas.getBoundingClientRect();
|
||||||
|
const sx = W / canvas.offsetWidth;
|
||||||
|
const n = nodeAt(
|
||||||
|
(e.clientX - r.left) * sx,
|
||||||
|
(e.clientY - r.top) * (H / canvas.offsetHeight),
|
||||||
|
);
|
||||||
|
if (n && !n.current) window.location.href = `/notes/${n.id}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => cancelAnimationFrame(animId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectHeadingAnchors() {
|
||||||
|
if (!document.getElementById("heading-anchor-styles")) {
|
||||||
|
const s = document.createElement("style");
|
||||||
|
s.id = "heading-anchor-styles";
|
||||||
|
s.textContent = `
|
||||||
|
.note-content h2, .note-content h3, .note-content h4 {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
.heading-anchor {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 0.4em;
|
||||||
|
color: oklch(38% 0 0);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 120ms, color 120ms;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.note-content h2:hover .heading-anchor,
|
||||||
|
.note-content h3:hover .heading-anchor,
|
||||||
|
.note-content h4:hover .heading-anchor { opacity: 1; }
|
||||||
|
.heading-anchor:hover, .heading-anchor.copied { color: oklch(71% 0.0863 296.59); opacity: 1; }
|
||||||
|
`;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll(".note-content h2, .note-content h3, .note-content h4")
|
||||||
|
.forEach((heading) => {
|
||||||
|
if (!heading.id || heading.querySelector(".heading-anchor")) return;
|
||||||
|
const anchor = document.createElement("a");
|
||||||
|
anchor.href = `#${heading.id}`;
|
||||||
|
anchor.className = "heading-anchor";
|
||||||
|
anchor.setAttribute("aria-label", "Copy link to section");
|
||||||
|
anchor.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
|
||||||
|
anchor.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const url = `${location.origin}${location.pathname}#${heading.id}`;
|
||||||
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
|
anchor.classList.add("copied");
|
||||||
|
setTimeout(() => anchor.classList.remove("copied"), 1800);
|
||||||
|
});
|
||||||
|
history.pushState(null, "", `#${heading.id}`);
|
||||||
|
});
|
||||||
|
heading.appendChild(anchor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSearch() {
|
||||||
|
const navItems = document.querySelectorAll<HTMLElement>(".nav-item");
|
||||||
|
document
|
||||||
|
.querySelectorAll<HTMLInputElement>("[data-search]")
|
||||||
|
.forEach((input) => {
|
||||||
|
input.addEventListener("input", (e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
const raw = target.value.toLowerCase().trim();
|
||||||
|
document
|
||||||
|
.querySelectorAll<HTMLInputElement>("[data-search]")
|
||||||
|
.forEach((o) => {
|
||||||
|
if (o !== target) o.value = target.value;
|
||||||
|
});
|
||||||
|
const isTag = raw.startsWith("#");
|
||||||
|
const search = isTag ? raw.slice(1) : raw;
|
||||||
|
navItems.forEach((item) => {
|
||||||
|
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";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (stopGraph) {
|
||||||
|
stopGraph();
|
||||||
|
stopGraph = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const graphDrawer = document.getElementById(
|
||||||
|
"graph-drawer",
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
if (!graphDrawer) return;
|
||||||
|
|
||||||
|
function onGraphDrawerChange() {
|
||||||
|
if (graphDrawer!.checked) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
stopGraph = startGraph() ?? null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (stopGraph) {
|
||||||
|
stopGraph();
|
||||||
|
stopGraph = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graphDrawer.addEventListener("change", onGraphDrawerChange);
|
||||||
|
|
||||||
|
const outerDrawer = graphDrawer.closest<HTMLElement>(".drawer.drawer-end");
|
||||||
|
const xlQuery = window.matchMedia("(min-width: 1280px)");
|
||||||
|
|
||||||
|
function setXlSidebar(open: boolean) {
|
||||||
|
if (!outerDrawer) return;
|
||||||
|
if (open) {
|
||||||
|
outerDrawer.classList.add("xl:drawer-open");
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
stopGraph = startGraph() ?? null;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
outerDrawer.classList.remove("xl:drawer-open");
|
||||||
|
if (stopGraph) {
|
||||||
|
stopGraph();
|
||||||
|
stopGraph = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const graphToggle = document.getElementById("graph-toggle");
|
||||||
|
graphToggle?.addEventListener("click", (e) => {
|
||||||
|
if (!xlQuery.matches) return;
|
||||||
|
e.preventDefault();
|
||||||
|
setXlSidebar(!outerDrawer?.classList.contains("xl:drawer-open"));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (xlQuery.matches) {
|
||||||
|
outerDrawer?.classList.add("xl:drawer-open");
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
stopGraph = startGraph() ?? null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
xlQuery.addEventListener("change", (e) => {
|
||||||
|
if (!e.matches) setXlSidebar(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
injectHeadingAnchors();
|
||||||
|
initSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("astro:page-load", init);
|
||||||
|
document.addEventListener("astro:before-preparation", () => {
|
||||||
|
if (stopGraph) {
|
||||||
|
stopGraph();
|
||||||
|
stopGraph = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
+4
-1
@@ -1,4 +1,7 @@
|
|||||||
export function getCategory(n: { id: string; data: { category?: string } }): string {
|
export function getCategory(n: {
|
||||||
|
id: string;
|
||||||
|
data: { category?: string };
|
||||||
|
}): string {
|
||||||
if (n.data.category) return n.data.category;
|
if (n.data.category) return n.data.category;
|
||||||
const parts = n.id.split("/");
|
const parts = n.id.split("/");
|
||||||
return parts.length > 1 ? parts[0] : "General";
|
return parts.length > 1 ? parts[0] : "General";
|
||||||
|
|||||||
Reference in New Issue
Block a user