mirror of
https://github.com/anotherhadi/blog.git
synced 2026-05-20 21:42:33 +02:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c521c7c7f9 | |||
| 25fb5a4bf0 | |||
| d257a0f26e | |||
| 3dfbdcf970 | |||
| 930c3bf3bb | |||
| a055640fa8 | |||
| 9c0bbc4b77 | |||
| 7968c662f6 | |||
| 99890dd1ef | |||
| db42928299 | |||
| 73b668b204 | |||
| c314445219 | |||
| b4b755b608 | |||
| 3e60ae5a35 | |||
| 4f64ccf706 | |||
| d6d410a2fa | |||
| a74f6b91d4 |
@@ -14,7 +14,6 @@
|
||||
"@types/bun": "^1.3.13",
|
||||
"astro": "6.1.9",
|
||||
"daisyui": "^5.5.19",
|
||||
"lucide-astro": "^0.556.0",
|
||||
"node-html-parser": "^7.1.0",
|
||||
"svelte": "^5.55.5",
|
||||
"tailwindcss": "^4.2.4",
|
||||
@@ -624,8 +623,6 @@
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
|
||||
|
||||
"lucide-astro": ["lucide-astro@0.556.0", "", { "peerDependencies": { "astro": ">=2.7.1" } }, "sha512-ugMjPb45AMfkLCaduNSbyy5NQEKvB1TxVVMmUS4S6L807PMESnX0Qp+DIKHjbyjJmPXOyLRbrzvR3YikTK7brg=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="],
|
||||
|
||||
@@ -1273,10 +1273,6 @@
|
||||
url = "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz";
|
||||
hash = "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==";
|
||||
};
|
||||
"lucide-astro@0.556.0" = fetchurl {
|
||||
url = "https://registry.npmjs.org/lucide-astro/-/lucide-astro-0.556.0.tgz";
|
||||
hash = "sha512-ugMjPb45AMfkLCaduNSbyy5NQEKvB1TxVVMmUS4S6L807PMESnX0Qp+DIKHjbyjJmPXOyLRbrzvR3YikTK7brg==";
|
||||
};
|
||||
"magic-string@0.30.21" = fetchurl {
|
||||
url = "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz";
|
||||
hash = "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==";
|
||||
|
||||
+2
-3
@@ -19,10 +19,9 @@
|
||||
"@lucide/astro": "^0.552.0",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"@types/bun": "^1.3.13",
|
||||
"astro": "6.1.9",
|
||||
"astro": "6.1.10",
|
||||
"daisyui": "^5.5.19",
|
||||
"lucide-astro": "^0.556.0",
|
||||
"node-html-parser": "^7.1.0",
|
||||
"node-html-parser": "^7.1.0",
|
||||
"svelte": "^5.55.5",
|
||||
"tailwindcss": "^4.2.4"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
import { ArrowUp } from "lucide-astro";
|
||||
import { ArrowUp } from "@lucide/astro";
|
||||
---
|
||||
|
||||
<button
|
||||
|
||||
@@ -18,14 +18,19 @@ const latestPosts = sortedPosts.slice(0, 3);
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-4xl font-bold mb-4">Latest Blog Posts</h2>
|
||||
<p class="text-lg text-base-content/70">
|
||||
Thoughts, insights, and tutorials on cybersecurity, OSINT, and
|
||||
technology.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{latestPosts.map((post) => <BlogCard post={post} />)}
|
||||
{
|
||||
latestPosts.map((post) => (
|
||||
<BlogCard
|
||||
displayBanner={false}
|
||||
displayTags={false}
|
||||
displayDate={false}
|
||||
post={post}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-12">
|
||||
|
||||
@@ -2,26 +2,23 @@
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import TagBadge from "./TagBadge.astro";
|
||||
import { Image } from "astro:assets";
|
||||
import { formatDate } from "../utils/notes";
|
||||
|
||||
interface Props {
|
||||
displayBanner: boolean;
|
||||
displayDate: boolean;
|
||||
displayTags: boolean;
|
||||
post: CollectionEntry<"blog">;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
|
||||
function formatDate(date: Date) {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
};
|
||||
return date.toLocaleDateString("en-US", options);
|
||||
}
|
||||
---
|
||||
|
||||
<article
|
||||
class="card bg-base-100 shadow-xl border border-base-200 rounded-lg hover:shadow-2xl transition-shadow"
|
||||
>
|
||||
|
||||
{ Astro.props.displayBanner && (
|
||||
<figure class="aspect-video">
|
||||
<Image
|
||||
src={post.data.image}
|
||||
@@ -31,25 +28,27 @@ function formatDate(date: Date) {
|
||||
height={400}
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
<div class="card-body">
|
||||
{ Astro.props.displayDate && (
|
||||
<time class="text-sm text-base-content/60">
|
||||
{formatDate(post.data.publishDate)}
|
||||
</time>
|
||||
)}
|
||||
<h2 class="card-title hover:text-primary transition-colors">
|
||||
<a href={`/blog/${post.id}`}>{post.data.title}</a>
|
||||
</h2>
|
||||
<p class="text-base-content/80">{post.data.description}</p>
|
||||
{
|
||||
post.data.tags && post.data.tags.length > 0 && (
|
||||
|
||||
{ Astro.props.displayTags && post.data.tags && post.data.tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
{post.data.tags.map((tag) => (
|
||||
<TagBadge tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<a href={`/blog/${post.id}`} class="btn btn-primary btn-sm">
|
||||
<a href={`/blog/${post.id}`} class="btn btn-soft btn-primary btn-sm">
|
||||
Read More
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ExternalLink, ChevronDown } from "@lucide/astro";
|
||||
|
||||
interface Props {
|
||||
displayBanner: boolean;
|
||||
repo: {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -34,6 +35,7 @@ const hasMultiplePlatforms = platforms.length > 1;
|
||||
<article
|
||||
class="card bg-base-100 shadow-xl border border-base-200 rounded-lg hover:shadow-2xl transition-shadow"
|
||||
>
|
||||
{ Astro.props.displayBanner && repo.banner_url && (
|
||||
<figure class="aspect-video bg-base-200 overflow-hidden">
|
||||
<img
|
||||
src={repo.banner_url}
|
||||
@@ -42,6 +44,7 @@ const hasMultiplePlatforms = platforms.length > 1;
|
||||
onerror="this.parentElement.style.display='none'"
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
|
||||
<div class="card-body">
|
||||
<h2 class="card-title hover:text-primary transition-colors">
|
||||
@@ -83,7 +86,7 @@ const hasMultiplePlatforms = platforms.length > 1;
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="btn btn-primary btn-sm gap-1"
|
||||
class="btn btn-soft btn-primary btn-sm gap-1"
|
||||
>
|
||||
<ExternalLink class="size-4" />
|
||||
View Source
|
||||
@@ -107,7 +110,7 @@ const hasMultiplePlatforms = platforms.length > 1;
|
||||
href={repo.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-primary btn-sm gap-1"
|
||||
class="btn btn-soft btn-primary btn-sm gap-1"
|
||||
>
|
||||
<ExternalLink class="size-4" />
|
||||
View on Gitea
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
---
|
||||
import { ArrowRight, FolderCode, Key, Rss } from "@lucide/astro";
|
||||
import { FolderCode, Key, Rss } from "@lucide/astro";
|
||||
import { Image } from "astro:assets";
|
||||
import type { SocialLinks } from "../config";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
avatar: any;
|
||||
location?: string;
|
||||
socialLinks?: {
|
||||
github?: string;
|
||||
gitlab?: string;
|
||||
gitea?: string;
|
||||
linkedin?: string;
|
||||
twitter?: string;
|
||||
bluesky?: string;
|
||||
instagram?: string;
|
||||
youTube?: string;
|
||||
medium?: string;
|
||||
kofi?: string;
|
||||
codetips?: string;
|
||||
};
|
||||
socialLinks?: SocialLinks;
|
||||
gpgKey?: string;
|
||||
rssFeed?: string;
|
||||
}
|
||||
@@ -30,18 +18,17 @@ const {
|
||||
title,
|
||||
description,
|
||||
avatar,
|
||||
location,
|
||||
socialLinks,
|
||||
gpgKey,
|
||||
rssFeed,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<section class="hero min-h-[65vh]">
|
||||
<div class="hero-content flex-col lg:flex-row-reverse max-w-7xl gap-8">
|
||||
<section class="hero py-20">
|
||||
<div class="hero-content flex-col lg:flex-row-reverse max-w-7xl gap-10">
|
||||
<div class="avatar">
|
||||
<div
|
||||
class="w-48 ring-primary ring-offset-base-100 rounded-full ring-2 ring-offset-2"
|
||||
class="w-32 md:w-48 ring-primary ring-offset-base-100 rounded-full ring-2 ring-offset-2"
|
||||
>
|
||||
<Image src={avatar} alt={name} />
|
||||
</div>
|
||||
@@ -51,8 +38,7 @@ const {
|
||||
Hi, I'm {name}
|
||||
</h1>
|
||||
<p class="text-xl text-base-content/80 mb-2">{title}</p>
|
||||
{location && <p class="text-base-content/60 mb-4">{location}</p>}
|
||||
<p class="text-lg leading-relaxed mb-6">
|
||||
<p class="text-lg max-w-lg leading-relaxed mb-6">
|
||||
{description}
|
||||
</p>
|
||||
{
|
||||
@@ -205,10 +191,10 @@ const {
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{socialLinks.youTube && (
|
||||
{socialLinks.youtube && (
|
||||
<div class="tooltip" data-tip="Youtube">
|
||||
<a
|
||||
href={socialLinks.youTube}
|
||||
href={socialLinks.youtube}
|
||||
class="btn btn-circle btn-ghost"
|
||||
aria-label="YouTube"
|
||||
target="_blank"
|
||||
@@ -346,25 +332,6 @@ const {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="mt-12 flex flex-wrap gap-5">
|
||||
<a href="/blog" class="btn btn-ghost gap-2">
|
||||
Blog Posts
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
<a href="/projects" class="btn btn-ghost gap-2">
|
||||
Projects
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
<a href="/notes" class="btn btn-ghost gap-2">
|
||||
Infosec Notes
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
<a href="/#contact" class="btn btn-ghost gap-2">
|
||||
Contact Me
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -110,8 +110,6 @@ function isActive(href: string) {
|
||||
if (!btn || !menu) return;
|
||||
const lines = btn.querySelectorAll<HTMLElement>(".hamburger-line");
|
||||
let open = false;
|
||||
|
||||
open = false;
|
||||
menu.style.display = "none";
|
||||
lines[0].style.transform = "";
|
||||
lines[1].style.opacity = "";
|
||||
|
||||
@@ -269,6 +269,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
|
||||
<canvas
|
||||
bind:this={canvas}
|
||||
height="190"
|
||||
|
||||
@@ -10,9 +10,10 @@ interface Props {
|
||||
graphEdges: { from: string; to: string }[];
|
||||
forwardLinks: CollectionEntry<"notes">[];
|
||||
backlinks: CollectionEntry<"notes">[];
|
||||
externalLinks: { url: string; label: string }[];
|
||||
}
|
||||
|
||||
const { entry, graphNodes, graphEdges, forwardLinks, backlinks } = Astro.props;
|
||||
const { entry, graphNodes, graphEdges, forwardLinks, backlinks, externalLinks } = Astro.props;
|
||||
---
|
||||
|
||||
<aside
|
||||
@@ -26,7 +27,7 @@ const { entry, graphNodes, graphEdges, forwardLinks, backlinks } = Astro.props;
|
||||
>
|
||||
graph
|
||||
</p>
|
||||
<NoteGraph client:visible nodes={graphNodes} edges={graphEdges} />
|
||||
<NoteGraph client:load nodes={graphNodes} edges={graphEdges} />
|
||||
{
|
||||
graphNodes.length < 2 && (
|
||||
<p class="font-mono text-[9px] text-base-content/20 text-center py-2">
|
||||
@@ -90,6 +91,31 @@ const { entry, graphNodes, graphEdges, forwardLinks, backlinks } = Astro.props;
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
externalLinks.length > 0 && (
|
||||
<div class="p-3 border-t border-base-300/40">
|
||||
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
|
||||
external
|
||||
</p>
|
||||
<ul class="space-y-1">
|
||||
{externalLinks.map(({ url, label }) => (
|
||||
<li>
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
|
||||
title={url}
|
||||
>
|
||||
↗ {label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="px-4 pt-4 pb-1 border-t border-base-300/40 mt-auto">
|
||||
<time
|
||||
datetime={entry.data.publishDate.toISOString()}
|
||||
|
||||
@@ -65,25 +65,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class="w-56 shrink-0 flex flex-col border-r border-base-300/60 h-[calc(100vh-3rem)]"
|
||||
style="background: oklch(4% 0 0);"
|
||||
>
|
||||
<!-- Mobile close bar -->
|
||||
<div class="lg:hidden flex items-center justify-between px-3 py-2 border-b border-base-300/40 shrink-0">
|
||||
<span class="font-mono text-[10px] text-base-content/30 uppercase tracking-widest">nav</span>
|
||||
<label
|
||||
for="nav-drawer"
|
||||
class="cursor-pointer text-base-content/30 hover:text-base-content/70 transition-colors p-1"
|
||||
aria-label="close sidebar"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="flex flex-col flex-1 min-h-0">
|
||||
<div class="px-3 py-3 border-b border-base-300/40 shrink-0">
|
||||
<div
|
||||
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md bg-base-200/50 border border-base-300/40 focus-within:border-base-300/70 transition-colors"
|
||||
@@ -98,7 +80,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nav -->
|
||||
<nav class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-2 py-2 space-y-px">
|
||||
{#each categories as cat}
|
||||
{@const catNotes = notes.filter(
|
||||
@@ -107,7 +88,6 @@
|
||||
{#if catNotes.length > 0 || !search}
|
||||
{@const isFolder = notes.some((n) => n.id.includes("/") && getCategory(n) === cat)}
|
||||
<div>
|
||||
<!-- Category header -->
|
||||
<div class="flex items-center w-full">
|
||||
<button
|
||||
onclick={() => toggle(cat)}
|
||||
@@ -142,7 +122,6 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Notes list -->
|
||||
{#if openCategories.includes(cat)}
|
||||
<ul
|
||||
class="ml-4 mt-0.5 pb-1 space-y-px"
|
||||
@@ -170,4 +149,4 @@
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import NoteNavContent from "./NoteNavContent.svelte";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
interface Props {
|
||||
notes: CollectionEntry<"notes">[];
|
||||
currentEntry?: CollectionEntry<"notes">;
|
||||
currentCategory?: string;
|
||||
categories: string[];
|
||||
}
|
||||
|
||||
const { notes, currentEntry, currentCategory, categories } = Astro.props;
|
||||
---
|
||||
|
||||
<aside
|
||||
class="w-56 shrink-0 flex flex-col border-r border-base-300/60 h-full"
|
||||
style="background: oklch(4% 0 0);"
|
||||
>
|
||||
<NoteNavContent
|
||||
client:load
|
||||
notes={notes}
|
||||
currentEntry={currentEntry}
|
||||
currentCategory={currentCategory}
|
||||
categories={categories}
|
||||
/>
|
||||
</aside>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { onMount, untrack } from "svelte";
|
||||
|
||||
interface Props {
|
||||
vars: string[];
|
||||
@@ -8,7 +8,7 @@
|
||||
const { vars }: Props = $props();
|
||||
|
||||
let values = $state<Record<string, string>>(
|
||||
Object.fromEntries(vars.map((v) => [v, ""])),
|
||||
untrack(() => Object.fromEntries(vars.map((v) => [v, ""]))),
|
||||
);
|
||||
let open = $state(false);
|
||||
let applied = $state(false);
|
||||
@@ -71,12 +71,14 @@
|
||||
{#each vars as v}
|
||||
<div class="flex items-center gap-3">
|
||||
<label
|
||||
for={`var-${v}`}
|
||||
class="font-mono text-xs text-primary/70 w-36 shrink-0 truncate"
|
||||
title={`$${v}`}
|
||||
>
|
||||
${v}
|
||||
</label>
|
||||
<input
|
||||
id={`var-${v}`}
|
||||
type="text"
|
||||
bind:value={values[v]}
|
||||
placeholder={`$${v}`}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
import { ArrowRight } from "@lucide/astro";
|
||||
---
|
||||
|
||||
<section id="notes" class="py-10 px-4">
|
||||
<div class="max-w-6xl mx-auto text-center">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-4xl font-bold mb-4">Infosec Notes</h2>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<p class="text-base text-base-content/70 mb-6">
|
||||
Cheatsheets and references on tools and techniques I use for CTFs and
|
||||
pentesting.
|
||||
</p>
|
||||
|
||||
<div class="text-center mt-12">
|
||||
<a href="/notes" class="btn btn-ghost gap-2">
|
||||
Browse Notes
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -10,14 +10,14 @@ const latestRepos = repos.slice(0, 3);
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-4xl font-bold mb-4">Check out my latest work</h2>
|
||||
<p class="text-lg text-base-content/70">
|
||||
I enjoy the challenge of reimagining existing programs & scripts in my
|
||||
own unique way.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{latestRepos.map((repo) => <GiteaProjectCard repo={repo} />)}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{
|
||||
latestRepos.map((repo) => (
|
||||
<GiteaProjectCard displayBanner={false} repo={repo} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-12">
|
||||
|
||||
+1
-3
@@ -9,7 +9,7 @@ export interface SocialLinks {
|
||||
twitter?: string;
|
||||
bluesky?: string;
|
||||
instagram?: string;
|
||||
youTube?: string;
|
||||
youtube?: string;
|
||||
codetips?: string;
|
||||
kofi?: string;
|
||||
medium?: string;
|
||||
@@ -23,7 +23,6 @@ export interface SiteConfig {
|
||||
title: string;
|
||||
description: string;
|
||||
avatar: string;
|
||||
location: string;
|
||||
socialLinks: SocialLinks;
|
||||
gpgKey?: string;
|
||||
rssFeed?: string;
|
||||
@@ -39,7 +38,6 @@ export const siteConfig: SiteConfig = {
|
||||
description:
|
||||
"Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧",
|
||||
avatar: "/avatar.png",
|
||||
location: "🇫🇷 France",
|
||||
socialLinks: {
|
||||
github: "https://github.com/anotherhadi",
|
||||
gitlab: "https://gitlab.com/anotherhadi_mirror",
|
||||
|
||||
@@ -55,8 +55,8 @@ Check for writable directories: you may be able to upload a webshell if FTP root
|
||||
## Brute Force
|
||||
|
||||
```bash
|
||||
hydra -l $user -P /usr/share/wordlists/rockyou.txt ftp://$IP
|
||||
medusa -h $IP -u $user -P /usr/share/wordlists/rockyou.txt -M ftp
|
||||
hydra -l $user -P ~/wordlists/rockyou.txt ftp://$IP
|
||||
medusa -h $IP -u $user -P ~/wordlists/rockyou.txt -M ftp
|
||||
```
|
||||
|
||||
Try default credentials first: `admin:admin`, `ftp:ftp`, `user:password`.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: "RDP"
|
||||
description: "Enumeration, exploitation and post-exploitation techniques for RDP servers."
|
||||
tags: ["rdp", "network", "service"]
|
||||
publishDate: 2026-05-04
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
RDP (Remote Desktop Protocol) runs on **port 3389** and provides a graphical remote session.
|
||||
Common on Windows servers and workstations.
|
||||
|
||||
## Enumeration
|
||||
|
||||
### Banner grabbing
|
||||
|
||||
```bash
|
||||
nmap -sV -p 3389 $IP
|
||||
nmap -p 3389 --script rdp-* $IP
|
||||
```
|
||||
|
||||
Key scripts:
|
||||
|
||||
- `rdp-enum-encryption`: checks encryption level
|
||||
- `rdp-vuln-ms12-020`: tests for MS12-020 DoS vulnerability
|
||||
|
||||
## Connect
|
||||
|
||||
```bash
|
||||
xfreerdp /u:$user /p:$password /v:$IP
|
||||
xfreerdp /u:$user /p:$password /v:$IP /cert:ignore
|
||||
rdesktop $IP
|
||||
```
|
||||
|
||||
Pass the hash directly (no plaintext password needed):
|
||||
|
||||
```bash
|
||||
xfreerdp /u:$user /pth:$hash /v:$IP
|
||||
```
|
||||
|
||||
## Brute Force
|
||||
|
||||
```bash
|
||||
hydra -l $user -P ~/wordlists/rockyou.txt rdp://$IP
|
||||
crowbar -b rdp -s $IP/32 -u $user -C ~/wordlists/rockyou.txt
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: "SSH"
|
||||
description: "Enumeration, exploitation and post-exploitation techniques for SSH servers."
|
||||
tags: ["ssh", "network", "service"]
|
||||
publishDate: 2026-05-04
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
SSH runs on **port 22** and provides an encrypted remote shell.
|
||||
Common implementations: OpenSSH, Dropbear, Bitvise.
|
||||
|
||||
## Enumeration
|
||||
|
||||
### Banner grabbing
|
||||
|
||||
```bash
|
||||
nc -nv $IP 22
|
||||
ssh $IP
|
||||
```
|
||||
|
||||
The banner reveals the software and version (e.g. `OpenSSH_9.2`).
|
||||
|
||||
### Nmap
|
||||
|
||||
```bash
|
||||
nmap -sV -p 22 $IP
|
||||
nmap -p 22 --script ssh-* $IP
|
||||
```
|
||||
|
||||
Key scripts:
|
||||
|
||||
- `ssh-hostkey`: retrieves the server's public key
|
||||
- `ssh-auth-methods`: lists accepted authentication methods
|
||||
- `ssh-brute`: brute-force credentials
|
||||
|
||||
## Connect
|
||||
|
||||
```bash
|
||||
ssh $user@$IP
|
||||
ssh -p 2222 $user@$IP
|
||||
ssh -i id_rsa $user@$IP
|
||||
```
|
||||
|
||||
## Brute Force
|
||||
|
||||
```bash
|
||||
hydra -l $user -P ~/wordlists/rockyou.txt ssh://$IP
|
||||
medusa -h $IP -u $user -P ~/wordlists/rockyou.txt -M ssh
|
||||
```
|
||||
|
||||
Only viable if password auth is enabled. Check with:
|
||||
|
||||
```bash
|
||||
ssh -v $user@$IP
|
||||
```
|
||||
|
||||
Look for `publickey,password` in the output.
|
||||
|
||||
## Key-Based Auth
|
||||
|
||||
If you find a private key (`id_rsa`), set permissions and connect:
|
||||
|
||||
```bash
|
||||
chmod 600 id_rsa
|
||||
ssh -i id_rsa $user@$IP
|
||||
```
|
||||
|
||||
If the key is encrypted, crack the passphrase:
|
||||
|
||||
```bash
|
||||
ssh2john id_rsa > hash.txt
|
||||
john hash.txt --wordlist=~/wordlists/rockyou.txt
|
||||
hashcat -m 22921 hash.txt ~/wordlists/rockyou.txt
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: "Telnet"
|
||||
description: "Enumeration, exploitation and post-exploitation techniques for Telnet servers."
|
||||
tags: ["telnet", "network", "service"]
|
||||
publishDate: 2026-05-04
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Telnet runs on **port 23** and transmits all data (including credentials) in **cleartext**.
|
||||
Common on embedded devices, legacy systems, routers, and IoT equipment.
|
||||
|
||||
## Enumeration
|
||||
|
||||
### Banner grabbing
|
||||
|
||||
```bash
|
||||
nc -nv $IP 23
|
||||
telnet $IP
|
||||
```
|
||||
|
||||
The banner often reveals the OS, hostname, or device type.
|
||||
|
||||
### Nmap
|
||||
|
||||
```bash
|
||||
nmap -sV -p 23 $IP
|
||||
nmap -p 23 --script telnet-* $IP
|
||||
```
|
||||
|
||||
Key scripts:
|
||||
|
||||
- `telnet-ntlm-info`: extracts NTLM info (Windows targets)
|
||||
- `telnet-brute`: brute-force credentials
|
||||
|
||||
## Connect
|
||||
|
||||
```bash
|
||||
telnet $IP
|
||||
telnet $IP 23
|
||||
```
|
||||
|
||||
Login with `user` / `password`. Session is fully interactive once authenticated.
|
||||
|
||||
## Brute Force
|
||||
|
||||
```bash
|
||||
hydra -l $user -P ~/wordlists/rockyou.txt telnet://$IP
|
||||
medusa -h $IP -u $user -P ~/wordlists/rockyou.txt -M telnet
|
||||
```
|
||||
|
||||
Try default credentials first. Routers and embedded devices commonly ship with `admin:admin`, `root:root`, or blank passwords.
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: "Information Gathering"
|
||||
description: "Essential cybersecurity cheatsheet for Information Gathering and Open Source Intelligence (OSINT). Discover data related to emails, domains, usernames, and images using both command line and online tools."
|
||||
tags: ["osint", "enumeration", "information-gathering"]
|
||||
publishDate: 2026-05-03
|
||||
---
|
||||
|
||||
**Information Gathering**, often referred to as **Open Source Intelligence (OSINT)** in the context of ethical hacking, is the systematic collection and analysis of publicly available data about a target, providing the foundational knowledge necessary to identify potential vulnerabilities and craft targeted security assessments.
|
||||
|
||||
## Command line tools
|
||||
|
||||
| **From** | **Use** |
|
||||
| --------- | ----------------------------------------------------------------------------------------------- |
|
||||
| Email | `holehe $email` |
|
||||
| | `ghunt email $email` (for google account) |
|
||||
| | `github-recon $email` ([link](http://github.com/anotherhadi/github-recon/), for github account) |
|
||||
| Domain | `theHarvester -d $domain -l 100` |
|
||||
| | `theHarvester -d $domain -l 100 -b all` (full) |
|
||||
| Username | `sherlock $username` |
|
||||
| Image | `exiftool $imagePath` |
|
||||
| Instagram | `instaloader profile $username` |
|
||||
| Github | `trufflehog github --org=$usernameOrOrg` |
|
||||
| | `github-recon $username` ([link](http://github.com/anotherhadi/github-recon/)) |
|
||||
|
||||
## Online tools
|
||||
|
||||
| **For** | **Use** |
|
||||
| ---------- | ------------------------------------------------------ |
|
||||
| Visualiser | [OSINTracker](https://www.osintracker.com/) |
|
||||
| IP | [Shodan](https://www.shodan.io/) |
|
||||
| | [Censys](https://search.censys.io/) |
|
||||
| Domain | [Whois](https://www.whois.com/whois/) |
|
||||
| | [crt.sh](https://crt.sh/) (certificate transparency) |
|
||||
| Name | [Webmii](https://webmii.com/) |
|
||||
| | [BreachDirectory](https://breachdirectory.org/) |
|
||||
| | [LeakLookup](https://leak-lookup.com/search) |
|
||||
| | [IntelX](https://intelx.io/) |
|
||||
| | [Genealogic.review](https://genealogic.review/) |
|
||||
| SSID | [Wigle](https://wigle.net/) |
|
||||
| Image | [PimEyes (faces)](https://pimeyes.com/) |
|
||||
| | [Lenso (faces)](https://lenso.ai) |
|
||||
| | [TinEye](https://tineye.com) |
|
||||
| | [Pic2Map (exif geolocation)](https://www.pic2map.com/) |
|
||||
| Username | [DeHashed](https://dehashed.com/search) |
|
||||
| | [BreachDirectory](https://breachdirectory.org/) |
|
||||
| | [IntelX](https://intelx.io/) |
|
||||
| | [LeakLookup](https://leak-lookup.com/search) |
|
||||
| | [Oathnet](https://oathnet.org/) |
|
||||
| Email | [DeHashed](https://dehashed.com/search) |
|
||||
| | [Hunter](https://hunter.io/) |
|
||||
| | [HaveIBeenPwned](https://haveibeenpwned.com/) |
|
||||
| | [BreachDirectory](https://breachdirectory.org/) |
|
||||
| | [LeakLookup](https://leak-lookup.com/search) |
|
||||
| | [IntelX](https://intelx.io/) |
|
||||
| | [Oathnet](https://oathnet.org/) |
|
||||
| Phone | [Epieos](https://epieos.com/) |
|
||||
| Instagram | [Dumpor](https://dumpor.io/) |
|
||||
| Misc | [Goosint](https://goosint.com/) |
|
||||
| | [OSINT Framework](https://osintframework.com/) |
|
||||
| | [OSINT Dojo](https://osintdojo.com/) |
|
||||
|
||||
## OSINT Aggregation Tool
|
||||
|
||||
<a href="https://iknowyou.hadi.icu" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>IKnowYou</h4>
|
||||
<p>Self-hosted OSINT aggregation platform: Run dozens of open-source intelligence tools against a single target in parallel; all from one clean web interface.</p>
|
||||
</span>
|
||||
</a>
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: "Sock Puppets"
|
||||
description: "Essential cheatsheet on creating and managing Sock Puppets (fake identities) for ethical security research and Open Source Intelligence (OSINT), focusing on maintaining separation from personal data and bypassing common verification."
|
||||
tags: ["osint", "sock-puppets"]
|
||||
publishDate: 2026-05-03
|
||||
---
|
||||
|
||||
Sock puppets are fake identities use to gather information from a target.
|
||||
The sock puppet should have no link between your personal information and the fakes ones. (No ip address, mail, follow, etc..)
|
||||
|
||||
## Information generation
|
||||
|
||||
<a href="https://fakerjs.dev" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Faker</h4>
|
||||
<p>Generate massive amounts of fake data</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://fakenamegenerator.com/" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Fake Name</h4>
|
||||
<p>Personal informations</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://www.thispersondoesnotexist.com/" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>This Person Does Not Exist</h4>
|
||||
<p>Generate fake image</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
## Bypass phone verification
|
||||
|
||||
<a href="https://www.smspool.net/" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>SMSPool</h4>
|
||||
<p>Cheapest and Fastest Online SMS verification</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://receive-sms-online.info" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Receive Sms Online</h4>
|
||||
<p>Free SMS verification</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://receivefreesms.net" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Receive Free Sms</h4>
|
||||
<p>Free SMS verification</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://receive-smss.com" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Receive Free Sms</h4>
|
||||
<p>Free SMS verification</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://onlinesim.io/" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Online Sim</h4>
|
||||
<p>SMS verification with free tier</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://sms4stats.com/" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Sms 4 Sats</h4>
|
||||
<p>Paid SMS verification</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="http://sms4sat6y7lkq4vscloomatwyj33cfeddukkvujo2hkdqtmyi465spid.onion" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Sms 4 Sats (Onion)</h4>
|
||||
<p>Paid SMS verification. Tor version</p>
|
||||
</span>
|
||||
</a>
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "Tips"
|
||||
description: "A cheatsheet of practical tips and unconventional methods for Open Source Intelligence (OSINT), focusing on advanced data visualization, information leakage detection, and utilizing web archives for historical data."
|
||||
tags: ["osint"]
|
||||
publishDate: 2026-05-03
|
||||
---
|
||||
|
||||
## Visualisation
|
||||
|
||||
Use [OSINTracker](https://app.osintracker.com/) to visualise your findings.
|
||||
It allows you to create a graph of your findings, which can help you see connections and relationships between different pieces of information.
|
||||
|
||||
## Forgotten passwords
|
||||
|
||||
To find email addresses and phone numbers associated with an account, you can click on "Forgot password?" on the login page of a website. Be careful, though, this creates notifications and can be detected by the target, and often gives your information away.
|
||||
|
||||
## Archive Search
|
||||
|
||||
- [Wayback Machine](https://web.archive.org) stores over 618 billion web captures
|
||||
- [Archive.ph](https://archive.ph) creates on-demand snapshots, including for JS-heavy sites, with both a functional page and screenshot version
|
||||
|
||||
## Google Cache
|
||||
|
||||
Google keeps a cached version of most indexed pages. Access it with the `cache:` operator:
|
||||
|
||||
```
|
||||
cache:example.com
|
||||
cache:example.com/page
|
||||
```
|
||||
|
||||
If the page has been taken down or modified, the cached version may still show the original content.
|
||||
|
||||
## Domain History
|
||||
|
||||
[VirusTotal](https://www.virustotal.com) shows the historical DNS records, subdomains, and associated IPs for any domain — useful when a site has moved or been taken down.
|
||||
|
||||
[ViewDNS.info](https://viewdns.info) covers WHOIS history, reverse IP, reverse MX, and port scans from a single interface.
|
||||
|
||||
## Bookmarklets
|
||||
|
||||
- [K2SOsint/Bookmarklets](https://github.com/K2SOsint/Bookmarklets)
|
||||
- [tools.myosint.training](https://tools.myosint.training/)
|
||||
+229
-117
@@ -7,7 +7,7 @@
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "anotherhadi@noreply.git.hadi.icu",
|
||||
"email": "1+anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
@@ -34,7 +34,7 @@
|
||||
"fork": false,
|
||||
"template": false,
|
||||
"mirror": true,
|
||||
"size": 429945,
|
||||
"size": 430163,
|
||||
"language": "Nix",
|
||||
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/nixy/languages",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi/nixy",
|
||||
@@ -47,13 +47,14 @@
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"branch_count": 1,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
"default_branch": "main",
|
||||
"archived": false,
|
||||
"created_at": "2026-03-30T17:31:04+02:00",
|
||||
"updated_at": "2026-04-23T09:52:11+02:00",
|
||||
"updated_at": "2026-05-07T09:47:53+02:00",
|
||||
"archived_at": "1970-01-01T01:00:00+01:00",
|
||||
"permissions": {
|
||||
"admin": false,
|
||||
@@ -90,7 +91,7 @@
|
||||
"internal": false,
|
||||
"mirror_interval": "8h0m0s",
|
||||
"object_format_name": "sha1",
|
||||
"mirror_updated": "2026-04-23T11:17:25+02:00",
|
||||
"mirror_updated": "2026-05-07T16:22:32+02:00",
|
||||
"topics": [
|
||||
"dotfiles",
|
||||
"hyprland",
|
||||
@@ -106,6 +107,220 @@
|
||||
},
|
||||
"banner_url": "https://git.hadi.icu/anotherhadi/nixy/raw/branch/main/.github/assets/banner.png"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"owner": {
|
||||
"id": 1,
|
||||
"login": "anotherhadi",
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "1+anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
"is_admin": false,
|
||||
"last_login": "0001-01-01T00:00:00Z",
|
||||
"created": "2026-03-30T17:21:50+02:00",
|
||||
"restricted": false,
|
||||
"active": false,
|
||||
"prohibit_login": false,
|
||||
"location": "127.0.0.1",
|
||||
"website": "https://hadi.icu",
|
||||
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
|
||||
"visibility": "public",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"starred_repos_count": 0,
|
||||
"username": "anotherhadi"
|
||||
},
|
||||
"name": "usbguard-tui",
|
||||
"full_name": "anotherhadi/usbguard-tui",
|
||||
"description": "A terminal UI for managing USB devices via usbguard. TUI built with golang & bubbletea.",
|
||||
"empty": false,
|
||||
"private": false,
|
||||
"fork": false,
|
||||
"template": false,
|
||||
"mirror": true,
|
||||
"size": 425,
|
||||
"language": "Go",
|
||||
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/usbguard-tui/languages",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi/usbguard-tui",
|
||||
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/usbguard-tui",
|
||||
"link": "",
|
||||
"ssh_url": "gitea@git.hadi.icu:anotherhadi/usbguard-tui.git",
|
||||
"clone_url": "https://git.hadi.icu/anotherhadi/usbguard-tui.git",
|
||||
"original_url": "https://github.com/anotherhadi/usbguard-tui",
|
||||
"website": "",
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"branch_count": 1,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
"default_branch": "main",
|
||||
"archived": false,
|
||||
"created_at": "2026-04-30T17:40:42+02:00",
|
||||
"updated_at": "2026-05-06T14:42:02+02:00",
|
||||
"archived_at": "1970-01-01T01:00:00+01:00",
|
||||
"permissions": {
|
||||
"admin": false,
|
||||
"push": false,
|
||||
"pull": true
|
||||
},
|
||||
"has_code": true,
|
||||
"has_issues": true,
|
||||
"internal_tracker": {
|
||||
"enable_time_tracker": true,
|
||||
"allow_only_contributors_to_track_time": true,
|
||||
"enable_issue_dependencies": true
|
||||
},
|
||||
"has_wiki": true,
|
||||
"has_pull_requests": false,
|
||||
"has_projects": true,
|
||||
"projects_mode": "all",
|
||||
"has_releases": true,
|
||||
"has_packages": true,
|
||||
"has_actions": false,
|
||||
"ignore_whitespace_conflicts": false,
|
||||
"allow_merge_commits": false,
|
||||
"allow_rebase": false,
|
||||
"allow_rebase_explicit": false,
|
||||
"allow_squash_merge": false,
|
||||
"allow_fast_forward_only_merge": false,
|
||||
"allow_rebase_update": false,
|
||||
"allow_manual_merge": true,
|
||||
"autodetect_manual_merge": false,
|
||||
"default_delete_branch_after_merge": false,
|
||||
"default_merge_style": "merge",
|
||||
"default_allow_maintainer_edit": false,
|
||||
"avatar_url": "",
|
||||
"internal": false,
|
||||
"mirror_interval": "8h0m0s",
|
||||
"object_format_name": "sha1",
|
||||
"mirror_updated": "2026-05-07T12:42:33+02:00",
|
||||
"topics": [
|
||||
"bubbletea",
|
||||
"tui",
|
||||
"usbguard"
|
||||
],
|
||||
"licenses": [
|
||||
"MIT"
|
||||
],
|
||||
"mirrors": {
|
||||
"github": "https://github.com/anotherhadi/usbguard-tui",
|
||||
"gitlab": "https://gitlab.com/anotherhadi_mirror/usbguard-tui"
|
||||
},
|
||||
"banner_url": "https://git.hadi.icu/anotherhadi/usbguard-tui/raw/branch/main/.github/assets/banner.png"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"owner": {
|
||||
"id": 1,
|
||||
"login": "anotherhadi",
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "1+anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
"is_admin": false,
|
||||
"last_login": "0001-01-01T00:00:00Z",
|
||||
"created": "2026-03-30T17:21:50+02:00",
|
||||
"restricted": false,
|
||||
"active": false,
|
||||
"prohibit_login": false,
|
||||
"location": "127.0.0.1",
|
||||
"website": "https://hadi.icu",
|
||||
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
|
||||
"visibility": "public",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"starred_repos_count": 0,
|
||||
"username": "anotherhadi"
|
||||
},
|
||||
"name": "blog",
|
||||
"full_name": "anotherhadi/blog",
|
||||
"description": "Thoughts, insights, and tutorials on cybersecurity, OSINT, and technology.",
|
||||
"empty": false,
|
||||
"private": false,
|
||||
"fork": false,
|
||||
"template": false,
|
||||
"mirror": true,
|
||||
"size": 3863,
|
||||
"language": "Nix",
|
||||
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog/languages",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi/blog",
|
||||
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog",
|
||||
"link": "",
|
||||
"ssh_url": "gitea@git.hadi.icu:anotherhadi/blog.git",
|
||||
"clone_url": "https://git.hadi.icu/anotherhadi/blog.git",
|
||||
"original_url": "https://github.com/anotherhadi/blog",
|
||||
"website": "https://hadi.icu",
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"branch_count": 1,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
"default_branch": "main",
|
||||
"archived": false,
|
||||
"created_at": "2026-03-30T17:39:47+02:00",
|
||||
"updated_at": "2026-05-04T17:03:05+02:00",
|
||||
"archived_at": "1970-01-01T01:00:00+01:00",
|
||||
"permissions": {
|
||||
"admin": false,
|
||||
"push": false,
|
||||
"pull": true
|
||||
},
|
||||
"has_code": true,
|
||||
"has_issues": true,
|
||||
"internal_tracker": {
|
||||
"enable_time_tracker": true,
|
||||
"allow_only_contributors_to_track_time": true,
|
||||
"enable_issue_dependencies": true
|
||||
},
|
||||
"has_wiki": true,
|
||||
"has_pull_requests": false,
|
||||
"has_projects": true,
|
||||
"projects_mode": "all",
|
||||
"has_releases": true,
|
||||
"has_packages": true,
|
||||
"has_actions": false,
|
||||
"ignore_whitespace_conflicts": false,
|
||||
"allow_merge_commits": false,
|
||||
"allow_rebase": false,
|
||||
"allow_rebase_explicit": false,
|
||||
"allow_squash_merge": false,
|
||||
"allow_fast_forward_only_merge": false,
|
||||
"allow_rebase_update": false,
|
||||
"allow_manual_merge": true,
|
||||
"autodetect_manual_merge": false,
|
||||
"default_delete_branch_after_merge": false,
|
||||
"default_merge_style": "merge",
|
||||
"default_allow_maintainer_edit": false,
|
||||
"avatar_url": "https://git.hadi.icu/repo-avatars/527f936266a33d3ccfe22013ef2ca0f61e20a4941912b16e4b4e9dcb14bf4e30",
|
||||
"internal": false,
|
||||
"mirror_interval": "8h0m0s",
|
||||
"object_format_name": "sha1",
|
||||
"mirror_updated": "2026-05-07T17:22:32+02:00",
|
||||
"topics": [
|
||||
"blog",
|
||||
"cybersecurity",
|
||||
"portfolio"
|
||||
],
|
||||
"licenses": [
|
||||
"MIT"
|
||||
],
|
||||
"mirrors": {
|
||||
"github": "https://github.com/anotherhadi/blog",
|
||||
"gitlab": "https://gitlab.com/anotherhadi_mirror/blog"
|
||||
},
|
||||
"banner_url": "https://git.hadi.icu/anotherhadi/blog/raw/branch/main/.github/assets/banner.png"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"owner": {
|
||||
@@ -114,7 +329,7 @@
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "anotherhadi@noreply.git.hadi.icu",
|
||||
"email": "1+anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
@@ -141,7 +356,7 @@
|
||||
"fork": false,
|
||||
"template": false,
|
||||
"mirror": true,
|
||||
"size": 676,
|
||||
"size": 681,
|
||||
"language": "Svelte",
|
||||
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/iknowyou/languages",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi/iknowyou",
|
||||
@@ -154,6 +369,7 @@
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"branch_count": 2,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
@@ -197,7 +413,7 @@
|
||||
"internal": false,
|
||||
"mirror_interval": "8h0m0s",
|
||||
"object_format_name": "sha1",
|
||||
"mirror_updated": "2026-04-23T14:17:25+02:00",
|
||||
"mirror_updated": "2026-05-07T20:02:34+02:00",
|
||||
"topics": [
|
||||
"osint",
|
||||
"osint-tool"
|
||||
@@ -211,112 +427,6 @@
|
||||
},
|
||||
"banner_url": "https://git.hadi.icu/anotherhadi/iknowyou/raw/branch/main/.github/assets/banner.png"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"owner": {
|
||||
"id": 1,
|
||||
"login": "anotherhadi",
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
"is_admin": false,
|
||||
"last_login": "0001-01-01T00:00:00Z",
|
||||
"created": "2026-03-30T17:21:50+02:00",
|
||||
"restricted": false,
|
||||
"active": false,
|
||||
"prohibit_login": false,
|
||||
"location": "127.0.0.1",
|
||||
"website": "https://hadi.icu",
|
||||
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
|
||||
"visibility": "public",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"starred_repos_count": 0,
|
||||
"username": "anotherhadi"
|
||||
},
|
||||
"name": "blog",
|
||||
"full_name": "anotherhadi/blog",
|
||||
"description": "Thoughts, insights, and tutorials on cybersecurity, OSINT, and technology.",
|
||||
"empty": false,
|
||||
"private": false,
|
||||
"fork": false,
|
||||
"template": false,
|
||||
"mirror": true,
|
||||
"size": 3009,
|
||||
"language": "Nix",
|
||||
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog/languages",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi/blog",
|
||||
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog",
|
||||
"link": "",
|
||||
"ssh_url": "gitea@git.hadi.icu:anotherhadi/blog.git",
|
||||
"clone_url": "https://git.hadi.icu/anotherhadi/blog.git",
|
||||
"original_url": "https://github.com/anotherhadi/blog",
|
||||
"website": "https://hadi.icu",
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
"default_branch": "main",
|
||||
"archived": false,
|
||||
"created_at": "2026-03-30T17:39:47+02:00",
|
||||
"updated_at": "2026-04-11T17:35:02+02:00",
|
||||
"archived_at": "1970-01-01T01:00:00+01:00",
|
||||
"permissions": {
|
||||
"admin": false,
|
||||
"push": false,
|
||||
"pull": true
|
||||
},
|
||||
"has_code": true,
|
||||
"has_issues": true,
|
||||
"internal_tracker": {
|
||||
"enable_time_tracker": true,
|
||||
"allow_only_contributors_to_track_time": true,
|
||||
"enable_issue_dependencies": true
|
||||
},
|
||||
"has_wiki": true,
|
||||
"has_pull_requests": false,
|
||||
"has_projects": true,
|
||||
"projects_mode": "all",
|
||||
"has_releases": true,
|
||||
"has_packages": true,
|
||||
"has_actions": false,
|
||||
"ignore_whitespace_conflicts": false,
|
||||
"allow_merge_commits": false,
|
||||
"allow_rebase": false,
|
||||
"allow_rebase_explicit": false,
|
||||
"allow_squash_merge": false,
|
||||
"allow_fast_forward_only_merge": false,
|
||||
"allow_rebase_update": false,
|
||||
"allow_manual_merge": true,
|
||||
"autodetect_manual_merge": false,
|
||||
"default_delete_branch_after_merge": false,
|
||||
"default_merge_style": "merge",
|
||||
"default_allow_maintainer_edit": false,
|
||||
"avatar_url": "https://git.hadi.icu/repo-avatars/527f936266a33d3ccfe22013ef2ca0f61e20a4941912b16e4b4e9dcb14bf4e30",
|
||||
"internal": false,
|
||||
"mirror_interval": "8h0m0s",
|
||||
"object_format_name": "sha1",
|
||||
"mirror_updated": "2026-04-23T11:17:26+02:00",
|
||||
"topics": [
|
||||
"blog",
|
||||
"cybersecurity",
|
||||
"portfolio"
|
||||
],
|
||||
"licenses": [
|
||||
"MIT"
|
||||
],
|
||||
"mirrors": {
|
||||
"github": "https://github.com/anotherhadi/blog",
|
||||
"gitlab": "https://gitlab.com/anotherhadi_mirror/blog"
|
||||
},
|
||||
"banner_url": "https://git.hadi.icu/anotherhadi/blog/raw/branch/main/.github/assets/banner.png"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"owner": {
|
||||
@@ -325,7 +435,7 @@
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "anotherhadi@noreply.git.hadi.icu",
|
||||
"email": "1+anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
@@ -352,7 +462,7 @@
|
||||
"fork": false,
|
||||
"template": false,
|
||||
"mirror": true,
|
||||
"size": 530,
|
||||
"size": 535,
|
||||
"language": "Nix",
|
||||
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/default-creds/languages",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi/default-creds",
|
||||
@@ -365,6 +475,7 @@
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"branch_count": 1,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
@@ -408,7 +519,7 @@
|
||||
"internal": false,
|
||||
"mirror_interval": "8h0m0s",
|
||||
"object_format_name": "sha1",
|
||||
"mirror_updated": "2026-04-23T11:17:25+02:00",
|
||||
"mirror_updated": "2026-05-07T17:12:33+02:00",
|
||||
"topics": [
|
||||
"cybersecurity",
|
||||
"cybersecurity-tools",
|
||||
@@ -432,7 +543,7 @@
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "anotherhadi@noreply.git.hadi.icu",
|
||||
"email": "1+anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
@@ -472,6 +583,7 @@
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"branch_count": 1,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
|
||||
@@ -6,6 +6,7 @@ import BackToTop from "../components/BackToTop.astro";
|
||||
import { ChevronLeft } from "@lucide/astro";
|
||||
import { parse } from "node-html-parser";
|
||||
import Author from "../components/Author.astro";
|
||||
import { formatDate } from "../utils/notes";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -19,14 +20,6 @@ interface Props {
|
||||
const { title, description, publishDate, updatedDate, image, tags } =
|
||||
Astro.props;
|
||||
|
||||
function formatDate(date: Date) {
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
const content = await Astro.slots.render("default");
|
||||
const wordCount = content.split(/\s+/).length;
|
||||
const readingTime = Math.ceil(wordCount / 200);
|
||||
|
||||
@@ -15,26 +15,11 @@ const {
|
||||
description = "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧",
|
||||
} = Astro.props;
|
||||
|
||||
// Custom blur-fade animation configuration
|
||||
const blurFadeAnimation = {
|
||||
old: {
|
||||
name: "blurFadeOut",
|
||||
duration: "0.1s",
|
||||
easing: "ease-in-out",
|
||||
fillMode: "forwards",
|
||||
},
|
||||
new: {
|
||||
name: "blurFadeIn",
|
||||
duration: "0.1s",
|
||||
easing: "ease-in-out",
|
||||
fillMode: "backwards",
|
||||
},
|
||||
};
|
||||
|
||||
const pageTransition = {
|
||||
forwards: blurFadeAnimation,
|
||||
backwards: blurFadeAnimation,
|
||||
const anim = {
|
||||
old: { name: "blurFadeOut", duration: "0.1s", easing: "ease-in-out", fillMode: "forwards" },
|
||||
new: { name: "blurFadeIn", duration: "0.1s", easing: "ease-in-out", fillMode: "backwards" },
|
||||
};
|
||||
const pageTransition = { forwards: anim, backwards: anim };
|
||||
|
||||
const origin = Astro.url.origin;
|
||||
---
|
||||
@@ -49,23 +34,19 @@ const origin = Astro.url.origin;
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
|
||||
<!-- View Transitions -->
|
||||
<ClientRouter />
|
||||
|
||||
<!-- Open Graph / Social Media -->
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={origin} />
|
||||
<meta property="og:image" content={`${origin}/images/og_home.png`} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={`${origin}/images/og_home.png`} />
|
||||
|
||||
<!-- RSS Feed -->
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
@@ -87,13 +68,11 @@ const origin = Astro.url.origin;
|
||||
<Oneko />
|
||||
<Console />
|
||||
|
||||
<!-- Smooth Scroll -->
|
||||
<style is:global>
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Initial Page Load Blur-Fade Animation */
|
||||
@keyframes pageLoadBlurFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -106,11 +85,10 @@ const origin = Astro.url.origin;
|
||||
}
|
||||
|
||||
html {
|
||||
animation: pageLoadBlurFade 0.3s ease-in-out;
|
||||
animation: pageLoadBlurFade 0.1s ease-in-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
/* Blur Fade View Transitions (for page-to-page navigation) */
|
||||
@keyframes blurFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
---
|
||||
import Layout from "./Layout.astro";
|
||||
import { Image } from "astro:assets";
|
||||
import TagBadge from "../components/TagBadge.astro";
|
||||
import { ChevronLeft, ExternalLink } from "@lucide/astro";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
image: any;
|
||||
tags: string[];
|
||||
demoLink?: string;
|
||||
url?: string;
|
||||
sourceLink?: string;
|
||||
}
|
||||
|
||||
const { title, description, image, tags, demoLink, url, sourceLink } =
|
||||
Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={`${title} - Another Hadi`} description={description}>
|
||||
<article class="max-w-4xl mx-auto px-4 py-20">
|
||||
<!-- Back button -->
|
||||
<div class="mb-8 flex flex-wrap justify-between gap-5">
|
||||
<a href="/projects" class="btn btn-ghost btn-sm">
|
||||
<ChevronLeft size={18} />
|
||||
Back to Projects
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Featured Image -->
|
||||
<!-- TODO: Future Enhancement - Support multiple images/project gallery -->
|
||||
{
|
||||
image && (
|
||||
<figure class="mb-8 rounded-2xl overflow-hidden">
|
||||
<Image
|
||||
src={image}
|
||||
alt={title}
|
||||
class="w-full aspect-video object-cover"
|
||||
width={1200}
|
||||
height={630}
|
||||
/>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Project Header -->
|
||||
<header class="mb-8">
|
||||
<h1 class="text-5xl font-bold mb-4">{title}</h1>
|
||||
<p class="text-xl text-base-content/70 mb-6">{description}</p>
|
||||
|
||||
<!-- Prominent Action Buttons -->
|
||||
{
|
||||
(demoLink || sourceLink) && (
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
{demoLink && (
|
||||
<a
|
||||
href={demoLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-primary gap-2"
|
||||
>
|
||||
<ExternalLink class="size-5" />
|
||||
Live Demo
|
||||
</a>
|
||||
)}
|
||||
{url && (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-soft gap-2"
|
||||
>
|
||||
<ExternalLink class="size-4" />
|
||||
Website
|
||||
</a>
|
||||
)}
|
||||
{sourceLink && (
|
||||
<a
|
||||
href={sourceLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-soft gap-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 32 32"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M16,2.345c7.735,0,14,6.265,14,14-.002,6.015-3.839,11.359-9.537,13.282-.7,.14-.963-.298-.963-.665,0-.473,.018-1.978,.018-3.85,0-1.312-.437-2.152-.945-2.59,3.115-.35,6.388-1.54,6.388-6.912,0-1.54-.543-2.783-1.435-3.762,.14-.35,.63-1.785-.14-3.71,0,0-1.173-.385-3.85,1.435-1.12-.315-2.31-.472-3.5-.472s-2.38,.157-3.5,.472c-2.677-1.802-3.85-1.435-3.85-1.435-.77,1.925-.28,3.36-.14,3.71-.892,.98-1.435,2.24-1.435,3.762,0,5.355,3.255,6.563,6.37,6.913-.403,.35-.77,.963-.893,1.872-.805,.368-2.818,.963-4.077-1.155-.263-.42-1.05-1.452-2.152-1.435-1.173,.018-.472,.665,.017,.927,.595,.332,1.277,1.575,1.435,1.978,.28,.787,1.19,2.293,4.707,1.645,0,1.173,.018,2.275,.018,2.607,0,.368-.263,.787-.963,.665-5.719-1.904-9.576-7.255-9.573-13.283,0-7.735,6.265-14,14-14Z" />
|
||||
</svg>
|
||||
View Source
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</header>
|
||||
|
||||
<!-- Tags -->
|
||||
{
|
||||
tags && tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-2 mt-4">
|
||||
{tags.map((tag) => (
|
||||
<TagBadge tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Project Content -->
|
||||
<div class="prose-content max-w-none">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider mt-12"></div>
|
||||
|
||||
<!-- Back to projects link -->
|
||||
<div class="flex justify-center gap-2 mt-12">
|
||||
<div class="flex gap-3 justify-center flex-wrap text-sm">
|
||||
<a href="/projects" class="link link-hover">View All Projects</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/#contact" class="link link-hover">Contact me</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="https://ko-fi.com/anotherhadi" class="link link-hover"
|
||||
>Support me</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</Layout>
|
||||
|
||||
<style is:global>
|
||||
.prose-content {
|
||||
color: inherit;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.prose-content h1,
|
||||
.prose-content h2,
|
||||
.prose-content h3,
|
||||
.prose-content h4,
|
||||
.prose-content h5,
|
||||
.prose-content h6 {
|
||||
font-weight: 700;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.prose-content h1 {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.prose-content h2 {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.prose-content h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.prose-content p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.prose-content a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.prose-content ul,
|
||||
.prose-content ol {
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 1.5rem;
|
||||
list-style-position: outside;
|
||||
}
|
||||
|
||||
.prose-content ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.prose-content ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.prose-content li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose-content code {
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: ui-monospace, monospace;
|
||||
}
|
||||
|
||||
.prose-content pre {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.prose-content pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.prose-content blockquote {
|
||||
border-left-width: 4px;
|
||||
padding-left: 1rem;
|
||||
font-style: italic;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.prose-content img {
|
||||
border-radius: 0.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.prose-content strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -45,7 +45,12 @@ const sortedPosts = blogPosts.sort(
|
||||
) : (
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{sortedPosts.map((post) => (
|
||||
<BlogCard post={post} />
|
||||
<BlogCard
|
||||
displayBanner={true}
|
||||
displayTags={true}
|
||||
displayDate={true}
|
||||
post={post}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,26 +3,31 @@ import Layout from "../layouts/Layout.astro";
|
||||
import Hero from "../components/Hero.astro";
|
||||
import Projects from "../components/Projects.astro";
|
||||
import Blog from "../components/Blog.astro";
|
||||
import Notes from "../components/Notes.astro";
|
||||
import Contact from "../components/Contact.astro";
|
||||
import { siteConfig } from "../config";
|
||||
import avatar from "../../public/avatar.jpg";
|
||||
---
|
||||
|
||||
<Layout title={`Another Hadi`} description={siteConfig.description}>
|
||||
<main>
|
||||
<main class="px-10">
|
||||
<Hero
|
||||
name={siteConfig.name}
|
||||
title={siteConfig.title}
|
||||
description={siteConfig.description}
|
||||
avatar={avatar}
|
||||
location={siteConfig.location}
|
||||
socialLinks={siteConfig.socialLinks}
|
||||
gpgKey={siteConfig.gpgKey}
|
||||
rssFeed={siteConfig.rssFeed}
|
||||
/>
|
||||
|
||||
<hr class="border-base-300/30 max-w-6xl mx-auto" />
|
||||
<Blog />
|
||||
<hr class="border-base-300/30 max-w-6xl mx-auto" />
|
||||
<Notes />
|
||||
<hr class="border-base-300/30 max-w-6xl mx-auto" />
|
||||
<Projects />
|
||||
<hr class="border-base-300/30 max-w-6xl mx-auto" />
|
||||
<Contact />
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
@@ -3,10 +3,10 @@ import { getCollection, render } from "astro:content";
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import { List, PanelRight } from "@lucide/astro";
|
||||
import NoteTOC from "../../components/NoteTOC.astro";
|
||||
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
|
||||
import NoteNavSidebar from "../../components/NoteNavSidebar.astro";
|
||||
import NoteGraphSidebar from "../../components/NoteGraphSidebar.astro";
|
||||
import NoteVars from "../../components/NoteVars.svelte";
|
||||
import { getCategory, extractLinks, extractHeadings } from "../../utils/notes";
|
||||
import { getCategory, extractLinks, extractExternalLinks } from "../../utils/notes";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const notes = await getCollection("notes");
|
||||
@@ -17,7 +17,7 @@ export async function getStaticPaths() {
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await render(entry);
|
||||
const { Content, headings: astroHeadings } = await render(entry);
|
||||
|
||||
const allNotes = await getCollection("notes");
|
||||
const sortedNotes = allNotes.sort((a, b) =>
|
||||
@@ -60,14 +60,22 @@ const noteVars = [
|
||||
),
|
||||
];
|
||||
|
||||
const headings = extractHeadings(entry.body ?? "");
|
||||
const headings = astroHeadings.map((h) => ({ depth: h.depth, text: h.text, id: h.slug }));
|
||||
const externalLinks = extractExternalLinks(entry.body ?? "");
|
||||
---
|
||||
|
||||
<style>
|
||||
.drawer.lg\:drawer-open > .drawer-side,
|
||||
.drawer.xl\:drawer-open > .drawer-side {
|
||||
top: 3rem;
|
||||
height: calc(100vh - 3rem);
|
||||
@media (min-width: 768px) {
|
||||
.drawer.md\:drawer-open > .drawer-side {
|
||||
top: 3rem;
|
||||
height: calc(100vh - 3rem);
|
||||
}
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.drawer.xl\:drawer-open > .drawer-side {
|
||||
top: 3rem;
|
||||
height: calc(100vh - 3rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -76,12 +84,12 @@ const headings = extractHeadings(entry.body ?? "");
|
||||
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 md:drawer-open min-h-[calc(100vh-3rem)]">
|
||||
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
<div class="drawer-content flex min-h-[calc(100vh-3rem)]">
|
||||
<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)] min-w-0">
|
||||
<div class="drawer drawer-end xl:drawer-open w-full" id="right-drawer">
|
||||
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
<div class="drawer-content flex flex-col min-w-0">
|
||||
<main class="flex-1 px-4 sm:px-6 lg:px-10 py-6 lg:py-10 min-w-0">
|
||||
@@ -115,7 +123,7 @@ const headings = extractHeadings(entry.body ?? "");
|
||||
<div class="flex items-center gap-2">
|
||||
<label
|
||||
for="nav-drawer"
|
||||
class="btn btn-ghost btn-xs lg:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||
class="btn btn-ghost btn-xs md:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||
>
|
||||
<List size={11} />
|
||||
nav
|
||||
@@ -124,7 +132,7 @@ const headings = extractHeadings(entry.body ?? "");
|
||||
<label
|
||||
for="graph-drawer"
|
||||
id="graph-toggle"
|
||||
class="btn btn-ghost btn-xs font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||
class="btn btn-ghost btn-xs xl:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||
title="Toggle graph"
|
||||
>
|
||||
<PanelRight size={11} />
|
||||
@@ -204,32 +212,29 @@ const headings = extractHeadings(entry.body ?? "");
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="drawer-side z-50">
|
||||
<div class="drawer-side z-[60]">
|
||||
<label
|
||||
for="nav-drawer"
|
||||
for="graph-drawer"
|
||||
aria-label="close sidebar"
|
||||
class="drawer-overlay"></label>
|
||||
<NoteNavSidebar
|
||||
client:load
|
||||
notes={sortedNotes}
|
||||
currentEntry={entry}
|
||||
categories={categories}
|
||||
<NoteGraphSidebar
|
||||
entry={entry}
|
||||
graphNodes={graphNodes}
|
||||
graphEdges={graphEdges}
|
||||
forwardLinks={forwardLinks}
|
||||
backlinks={backlinks}
|
||||
externalLinks={externalLinks}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-side z-40">
|
||||
<label
|
||||
for="graph-drawer"
|
||||
aria-label="close sidebar"
|
||||
class="drawer-overlay xl:hidden"></label>
|
||||
<NoteGraphSidebar
|
||||
entry={entry}
|
||||
graphNodes={graphNodes}
|
||||
graphEdges={graphEdges}
|
||||
forwardLinks={forwardLinks}
|
||||
backlinks={backlinks}
|
||||
<div class="drawer-side z-[70]">
|
||||
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<NoteNavSidebar
|
||||
notes={sortedNotes}
|
||||
currentEntry={entry}
|
||||
categories={categories}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,7 +247,9 @@ const headings = extractHeadings(entry.body ?? "");
|
||||
const s = document.createElement("style");
|
||||
s.id = "heading-anchor-styles";
|
||||
s.textContent = `
|
||||
.note-content h2, .note-content h3, .note-content h4 {
|
||||
.note-content h2:not(.link-card h2),
|
||||
.note-content h3:not(.link-card h3),
|
||||
.note-content h4:not(.link-card h4) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
@@ -288,51 +295,7 @@ const headings = extractHeadings(entry.body ?? "");
|
||||
});
|
||||
}
|
||||
|
||||
function initXlGraphToggle() {
|
||||
const graphDrawer = document.getElementById(
|
||||
"graph-drawer",
|
||||
) as HTMLInputElement | null;
|
||||
if (!graphDrawer) return;
|
||||
|
||||
const outerDrawer = graphDrawer.closest<HTMLElement>(".drawer.drawer-end");
|
||||
const xlQuery = window.matchMedia("(min-width: 1280px)");
|
||||
const STORAGE_KEY = "notes-graph-sidebar";
|
||||
|
||||
function setXlSidebar(open: boolean) {
|
||||
if (!outerDrawer) return;
|
||||
if (open) {
|
||||
outerDrawer.classList.add("xl:drawer-open");
|
||||
} else {
|
||||
outerDrawer.classList.remove("xl:drawer-open");
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, open ? "1" : "0");
|
||||
}
|
||||
|
||||
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) {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
// Open by default unless user explicitly closed it
|
||||
setXlSidebar(saved !== "0");
|
||||
}
|
||||
|
||||
xlQuery.addEventListener("change", (e) => {
|
||||
if (!e.matches) {
|
||||
outerDrawer?.classList.remove("xl:drawer-open");
|
||||
} else {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
setXlSidebar(saved !== "0");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
injectHeadingAnchors();
|
||||
initXlGraphToggle();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
|
||||
import NoteNavSidebar from "../../components/NoteNavSidebar.astro";
|
||||
import { getCategory } from "../../utils/notes";
|
||||
import { List } from "@lucide/astro";
|
||||
|
||||
@@ -41,9 +41,11 @@ if (!categoryNotes) {
|
||||
---
|
||||
|
||||
<style>
|
||||
.drawer.lg\:drawer-open > .drawer-side {
|
||||
top: 3rem;
|
||||
height: calc(100vh - 3rem);
|
||||
@media (min-width: 768px) {
|
||||
.drawer.md\:drawer-open > .drawer-side {
|
||||
top: 3rem;
|
||||
height: calc(100vh - 3rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -52,7 +54,7 @@ if (!categoryNotes) {
|
||||
description={`Notes on ${category}.`}
|
||||
>
|
||||
<main class="max-w-screen-2xl mx-auto">
|
||||
<div class="drawer lg:drawer-open min-h-[calc(100vh-3rem)]">
|
||||
<div class="drawer md:drawer-open min-h-[calc(100vh-3rem)]">
|
||||
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
<div class="drawer-content flex flex-col min-w-0">
|
||||
@@ -72,7 +74,7 @@ if (!categoryNotes) {
|
||||
</div>
|
||||
<label
|
||||
for="nav-drawer"
|
||||
class="btn btn-ghost btn-xs lg:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||
class="btn btn-ghost btn-xs md:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||
>
|
||||
<List size={11} />
|
||||
nav
|
||||
@@ -161,13 +163,9 @@ if (!categoryNotes) {
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="drawer-side z-50">
|
||||
<label
|
||||
for="nav-drawer"
|
||||
aria-label="close sidebar"
|
||||
class="drawer-overlay"></label>
|
||||
<div class="drawer-side z-[70]">
|
||||
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<NoteNavSidebar
|
||||
client:load
|
||||
notes={allNotes}
|
||||
currentCategory={category}
|
||||
categories={categories}
|
||||
|
||||
@@ -36,7 +36,7 @@ import repos from "../../data/repos.json";
|
||||
) : (
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{repos.map((repo) => (
|
||||
<GiteaProjectCard repo={repo} />
|
||||
<GiteaProjectCard displayBanner={true} repo={repo} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -36,6 +36,71 @@
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
.drawer-side > aside > astro-island {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.btn:not(.btn-circle):not(.btn-square) {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.link-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.875rem;
|
||||
border-radius: var(--radius-box);
|
||||
border: 1px solid oklch(24% 0 0);
|
||||
background: transparent;
|
||||
color: var(--color-base-content);
|
||||
text-decoration: none !important;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
margin-block: 0.25rem;
|
||||
}
|
||||
.link-card::after {
|
||||
content: "↗";
|
||||
margin-left: auto;
|
||||
padding-left: 0.75rem;
|
||||
opacity: 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 0.75rem;
|
||||
transition:
|
||||
opacity 0.15s ease,
|
||||
transform 0.15s ease;
|
||||
transform: translate(-4px, 4px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.link-card:hover {
|
||||
background: var(--color-base-200);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
.link-card:hover::after {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
.link-card span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.link-card h4 {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
.link-card:hover h4 {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.link-card p {
|
||||
font-size: 0.75rem;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
color: oklch(52% 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-11
@@ -7,16 +7,7 @@ export function getCategory(n: {
|
||||
return parts.length > 1 ? parts[0] : "General";
|
||||
}
|
||||
|
||||
export function extractInlineHashtags(body: string): string[] {
|
||||
const re = /#(\w+)/g;
|
||||
const tags: string[] = [];
|
||||
let m;
|
||||
while ((m = re.exec(body)) !== null) tags.push(m[1].toLowerCase());
|
||||
return [...new Set(tags)];
|
||||
}
|
||||
|
||||
// Mirrors github-slugger: keeps _, keeps unicode letters/numbers, spaces → hyphens
|
||||
export function slugify(text: string): string {
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
|
||||
@@ -24,6 +15,34 @@ export function slugify(text: string): string {
|
||||
.replace(/ +/g, "-");
|
||||
}
|
||||
|
||||
export function extractExternalLinks(body: string): { url: string; label: string }[] {
|
||||
const seen = new Set<string>();
|
||||
const links: { url: string; label: string }[] = [];
|
||||
|
||||
// Markdown: [label](https://...)
|
||||
const mdRe = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g;
|
||||
let m;
|
||||
while ((m = mdRe.exec(body)) !== null) {
|
||||
if (!seen.has(m[2])) {
|
||||
seen.add(m[2]);
|
||||
links.push({ url: m[2], label: m[1] });
|
||||
}
|
||||
}
|
||||
|
||||
// HTML: <a href="https://...">...</a> — use h4 content as label if present, else href host
|
||||
const htmlRe = /<a\s[^>]*href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
|
||||
while ((m = htmlRe.exec(body)) !== null) {
|
||||
const url = m[1];
|
||||
if (seen.has(url)) continue;
|
||||
seen.add(url);
|
||||
const h4 = m[2].match(/<h4[^>]*>([\s\S]*?)<\/h4>/);
|
||||
const label = h4 ? h4[1].trim() : new URL(url).hostname;
|
||||
links.push({ url, label });
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
export function extractLinks(body: string): string[] {
|
||||
const re = /\(\/notes\/([^)#\s]+)(?:#[^)\s]*)?\)/g;
|
||||
const ids: string[] = [];
|
||||
@@ -34,7 +53,7 @@ export function extractLinks(body: string): string[] {
|
||||
|
||||
export function formatDate(date: Date): string {
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user