diff --git a/public/scripts/oneko.js b/public/scripts/oneko.js index 648b17b..4a814b9 100644 --- a/public/scripts/oneko.js +++ b/public/scripts/oneko.js @@ -3,57 +3,85 @@ (function oneko() { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; - const SIZE = 32; + const SIZE = 32; const SPEED = 10; const spriteSets = { - idle: [[-3, -3]], - alert: [[-7, -3]], - scratchSelf: [[-5, 0], [-6, 0], [-7, 0]], - scratchWallE:[[-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]], + idle: [[-3, -3]], + alert: [[-7, -3]], + scratchSelf: [ + [-5, 0], + [-6, 0], + [-7, 0], + ], + scratchWallE: [ + [-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"); if (!track) return; const el = document.createElement("div"); - el.id = "oneko"; - el.ariaHidden = "true"; - el.style.width = `${SIZE}px`; - el.style.height = `${SIZE}px`; - el.style.position = "absolute"; - el.style.bottom = "0"; - el.style.pointerEvents = "none"; + el.id = "oneko"; + el.ariaHidden = "true"; + el.style.width = `${SIZE}px`; + el.style.height = `${SIZE}px`; + el.style.position = "absolute"; + el.style.bottom = "0"; + el.style.pointerEvents = "none"; el.style.imageRendering = "pixelated"; - el.style.zIndex = "2147483647"; - el.style.backgroundImage= "url(/oneko.gif)"; + el.style.zIndex = "2147483647"; + el.style.backgroundImage = "url(/oneko.gif)"; track.appendChild(el); - function maxX() { return track.offsetWidth - SIZE; } - function clamp(v,lo,hi) { return Math.max(lo, Math.min(hi, v)); } - function randomTarget() { return Math.random() * maxX(); } + function maxX() { + return track.offsetWidth - SIZE; + } + function clamp(v, lo, hi) { + return Math.max(lo, Math.min(hi, v)); + } + function randomTarget() { + return Math.random() * maxX(); + } function setSprite(name, frame) { const s = spriteSets[name][frame % spriteSets[name].length]; el.style.backgroundPosition = `${s[0] * SIZE}px ${s[1] * SIZE}px`; } - let posX = randomTarget(); - let targetX = posX; - el.style.left = `${posX}px`; + let posX = randomTarget(); + let targetX = posX; + el.style.left = `${posX}px`; - let frameCount = 0; - let idleTime = 0; - let idleAnim = null; + let frameCount = 0; + let idleTime = 0; + let idleAnim = null; let idleAnimFrame = 0; - let lastTs = null; + let lastTs = null; - function resetIdle() { idleAnim = null; idleAnimFrame = 0; } + function resetIdle() { + idleAnim = null; + idleAnimFrame = 0; + } function idle() { idleTime++; @@ -65,16 +93,23 @@ 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"]; - if (posX <= SIZE) opts.push("scratchWallW"); + if (posX <= SIZE) opts.push("scratchWallW"); if (posX >= maxX() - SIZE) opts.push("scratchWallE"); idleAnim = opts[Math.floor(Math.random() * opts.length)]; } switch (idleAnim) { case "sleeping": - if (idleAnimFrame < 8) { setSprite("tired", 0); break; } + if (idleAnimFrame < 8) { + setSprite("tired", 0); + break; + } setSprite("sleeping", Math.floor(idleAnimFrame / 4)); if (idleAnimFrame > 192) resetIdle(); break; @@ -119,7 +154,10 @@ function loop(ts) { if (!el.isConnected) return; if (!lastTs) lastTs = ts; - if (ts - lastTs > 100) { lastTs = ts; frame(); } + if (ts - lastTs > 100) { + lastTs = ts; + frame(); + } window.requestAnimationFrame(loop); } @@ -128,14 +166,19 @@ const newWidth = track.offsetWidth; if (lastTrackWidth > 0) { const ratio = newWidth / lastTrackWidth; - posX = clamp(posX * ratio, 0, maxX()); + posX = clamp(posX * ratio, 0, maxX()); targetX = clamp(targetX * ratio, 0, maxX()); el.style.left = `${posX}px`; } lastTrackWidth = newWidth; }); - setTimeout(() => { targetX = randomTarget(); }, 800 + Math.random() * 1500); + setTimeout( + () => { + targetX = randomTarget(); + }, + 800 + Math.random() * 1500, + ); window.requestAnimationFrame(loop); })(); diff --git a/scripts/fetch-repos.ts b/scripts/fetch-repos.ts index 76d8bcf..d44f13e 100644 --- a/scripts/fetch-repos.ts +++ b/scripts/fetch-repos.ts @@ -53,28 +53,24 @@ async function checkMirrors(repoName: string): Promise { export async function fetchGiteaRepos(): Promise { try { 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}`); const repos: GiteaRepo[] = await res.json(); const filtered = repos - .filter((r) => - !r.fork && - !r.private && - !SKIP_REPOS.includes(r.name) - ) + .filter((r) => !r.fork && !r.private && !SKIP_REPOS.includes(r.name)) .sort( (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( filtered.map(async (repo) => ({ ...repo, mirrors: await checkMirrors(repo.name), - })) + })), ); return reposWithMirrors; @@ -91,18 +87,15 @@ export function getBannerUrl(repo: GiteaRepo): string { async function main() { console.log("Fetching repos from Gitea..."); const rawRepos = await fetchGiteaRepos(); - const repos = rawRepos.map(repo => ({ + const repos = rawRepos.map((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"); await mkdir(dataDir, { recursive: true }); - await Bun.write( - join(dataDir, "repos.json"), - JSON.stringify(repos, null, 2) - ); + await Bun.write(join(dataDir, "repos.json"), JSON.stringify(repos, null, 2)); console.log(`Saved ${repos.length} repos to src/data/repos.json`); } diff --git a/src/components/Author.astro b/src/components/Author.astro index a6cabb3..ef5559c 100644 --- a/src/components/Author.astro +++ b/src/components/Author.astro @@ -1,18 +1,26 @@ --- import { Image } from "astro:assets"; const avatar = "/avatar.jpg"; -const username = "anotherhadi" -const bio = "Infosec engineer." - +const username = "anotherhadi"; +const bio = "Infosec engineer."; --- +
- anotherhadi avatar + anotherhadi avatar
-

@{username}

+

+ @{username} +

{bio}

diff --git a/src/components/GiteaProjectCard.astro b/src/components/GiteaProjectCard.astro index d2cc052..983c249 100644 --- a/src/components/GiteaProjectCard.astro +++ b/src/components/GiteaProjectCard.astro @@ -19,8 +19,12 @@ interface Props { const { repo } = Astro.props; const platforms = [ - ...(repo.mirrors.github ? [{ label: "GitHub", url: repo.mirrors.github }] : []), - ...(repo.mirrors.gitlab ? [{ label: "GitLab", url: repo.mirrors.gitlab }] : []), + ...(repo.mirrors.github + ? [{ label: "GitHub", url: repo.mirrors.github }] + : []), + ...(repo.mirrors.gitlab + ? [{ label: "GitLab", url: repo.mirrors.gitlab }] + : []), { label: "Gitea", url: repo.html_url }, ]; @@ -46,64 +50,70 @@ const hasMultiplePlatforms = platforms.length > 1; - {repo.description && ( -

{repo.description}

- )} + {repo.description &&

{repo.description}

}
- {repo.topics.map((topic) => ( - - {topic} - - ))} + { + repo.topics.map((topic) => ( + + {topic} + + )) + }
- {repo.website && ( - - - - Website - - )} - - {hasMultiplePlatforms ? ( - - ) : ( - - - - View on Gitea - - )} + + Website + + ) + } + + { + hasMultiplePlatforms ? ( + + ) : ( + + + View on Gitea + + ) + }
diff --git a/src/components/Hero.astro b/src/components/Hero.astro index 519a82b..fb10cce 100644 --- a/src/components/Hero.astro +++ b/src/components/Hero.astro @@ -25,8 +25,16 @@ interface Props { rssFeed?: string; } -const { name, title, description, avatar, location, socialLinks, gpgKey, rssFeed } = - Astro.props; +const { + name, + title, + description, + avatar, + location, + socialLinks, + gpgKey, + rssFeed, +} = Astro.props; ---
@@ -80,34 +88,40 @@ const { name, title, description, avatar, location, socialLinks, gpgKey, rssFeed class="btn btn-circle btn-ghost" aria-label="Gitlab" > - - + role="img" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + > + )} {socialLinks.gitea && ( - + )} {socialLinks.linkedin && (
diff --git a/src/components/Navbar.astro b/src/components/Navbar.astro index 29bf0b4..91b4a27 100644 --- a/src/components/Navbar.astro +++ b/src/components/Navbar.astro @@ -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" style="background: oklch(0% 0 0 / 0.85); backdrop-filter: blur(12px); border-bottom: 1px solid oklch(22% 0 0);" > -
+ @@ -113,7 +122,11 @@ function isActive(href: string) { }); 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; menu.style.display = "none"; lines[0].style.transform = ""; diff --git a/src/layouts/BlogLayout.astro b/src/layouts/BlogLayout.astro index 3781d1a..c4ede24 100644 --- a/src/layouts/BlogLayout.astro +++ b/src/layouts/BlogLayout.astro @@ -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 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 headers = root.querySelectorAll("h1, h2, h3"); @@ -38,14 +37,14 @@ const headers = root.querySelectorAll("h1, h2, h3"); const toc = headers.map((header) => ({ depth: parseInt(header.tagName.replace("H", "")), text: header.innerText.trim(), - slug: header.getAttribute("id"), // Astro génère l'id automatiquement + slug: header.getAttribute("id"), })); ---
- + - { - image && ( -
- {title} -
- ) + image && ( +
+ {title} +
+ ) } -

{title}

{description}

@@ -82,64 +79,58 @@ const toc = headers.map((header) => ({ {readingTime} min read { - updatedDate && ( - <> - - Updated: {formatDate(updatedDate)} - - ) + updatedDate && ( + <> + + Updated: {formatDate(updatedDate)} + + ) }
{ - tags && tags.length > 0 && ( -
- {tags.map((tag) => ( - - ))} -
- ) + tags && tags.length > 0 && ( +
+ {tags.map((tag) => ( + + ))} +
+ ) } -
- { - toc.length > 0 && ( -
- - -

- Table of Contents -

-
- -
-
- - ) + toc.length > 0 && ( +
+ +

+ Table of Contents +

+
+ +
+
+ ) } -
- diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index c69fa17..fa82753 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -78,8 +78,7 @@ const origin = Astro.url.origin; defer src="https://umami.hadi.icu/script.js" data-website-id="91b0c3a1-130a-4974-be47-078bc092cec8" - data-domains="hadi.icu,www.hadi.icu" - > + data-domains="hadi.icu,www.hadi.icu"> diff --git a/src/layouts/ProjectLayout.astro b/src/layouts/ProjectLayout.astro index 5c699e1..57f666a 100644 --- a/src/layouts/ProjectLayout.astro +++ b/src/layouts/ProjectLayout.astro @@ -124,9 +124,7 @@ const { title, description, image, tags, demoLink, url, sourceLink } =
- View All Projects + View All Projects Contact me diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index 5ec9ea3..c2f1e77 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -10,7 +10,6 @@ const blogPosts = await getCollection("blog"); const sortedPosts = blogPosts.sort( (a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(), ); - --- 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(); 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[] { - // Capture slug before optional #fragment: (/notes/slug) or (/notes/slug#section) const re = /\(\/notes\/([^)#\s]+)(?:#[^)\s]*)?\)/g; const ids: string[] = []; let m; @@ -33,12 +38,14 @@ function extractLinks(body: string): string[] { 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] ?? []) .map((id) => allNotes.find((n) => n.id === id)) .filter(Boolean) as typeof allNotes; 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 = [ @@ -53,34 +60,32 @@ const graphEdges = [ ...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) { return text .toLowerCase() - .replace(/[^\p{L}\p{N}\s_-]/gu, "") // keep letters (unicode), numbers, spaces, _, - + .replace(/[^\p{L}\p{N}\s_-]/gu, "") .trim() - .replace(/ +/g, "-"); // spaces → hyphens (github-slugger does exactly this) + .replace(/ +/g, "-"); } const headings: { depth: number; text: string; id: string }[] = []; const headingRe = /^(#{2,4}) (.+)$/gm; let hm; while ((hm = headingRe.exec(entry.body ?? "")) !== null) { - // Strip markdown formatting while preserving literal _ (word-internal underscores like my_var) - // Paired markers are stripped to their content; lone * are removed; _ only stripped at word boundaries - const raw = hm[2].trim() - .replace(/`[^`]*`/g, "") // `code` → remove - .replace(/\*\*(.*?)\*\*/g, "$1") // **bold** → text - .replace(/(? - /* Both sidebars sit below the navbar when in drawer-open mode */ .drawer.lg\:drawer-open > .drawer-side, .drawer.xl\:drawer-open > .drawer-side { top: 3rem; @@ -92,522 +97,327 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) { title={`${entry.data.title} — Security Notes`} description={entry.data.description} > +
+
+ -
- +
+
+ -
- -
- - -
-
-
-
- -
- - + + +
+
+
+ +
+ + +
+
+ + +
-
- - -
- - - - - + + diff --git a/src/pages/notes/index.astro b/src/pages/notes/index.astro index d6526fd..66cca06 100644 --- a/src/pages/notes/index.astro +++ b/src/pages/notes/index.astro @@ -6,7 +6,7 @@ import { getCategory } from "../../utils/notes"; const notes = await getCollection("notes"); 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(); @@ -15,7 +15,7 @@ const searchIndex = Object.fromEntries( sortedNotes.map((n) => [ n.id, [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." >
- -
+
- security notes + security notes

Notes

@@ -36,8 +38,10 @@ const searchIndex = Object.fromEntries(

-
-
+
+
-
+
{ categories.map((cat) => { const catNotes = sortedNotes.filter((n) => getCategory(n) === cat); return (
-
+

- /{cat} + / + {cat}

{catNotes.length} note{catNotes.length !== 1 ? "s" : ""} @@ -67,7 +72,7 @@ const searchIndex = Object.fromEntries(