mirror of
https://github.com/anotherhadi/blog.git
synced 2026-04-02 19:52:10 +02:00
init
This commit is contained in:
179
src/layouts/BlogLayout.astro
Normal file
179
src/layouts/BlogLayout.astro
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
import Layout from "./Layout.astro";
|
||||
import { Image } from "astro:assets";
|
||||
import TagBadge from "../components/TagBadge.astro";
|
||||
import BackToTop from "../components/BackToTop.astro";
|
||||
import { ChevronLeft } from "@lucide/astro";
|
||||
import { parse } from "node-html-parser";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
publishDate: Date;
|
||||
updatedDate?: Date;
|
||||
image: any;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
const { title, description, publishDate, updatedDate, image, tags } =
|
||||
Astro.props;
|
||||
|
||||
function formatDate(date: Date) {
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
// 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 root = parse(content);
|
||||
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
|
||||
}));
|
||||
---
|
||||
|
||||
<Layout title={`${title} - Another Hadi`} description={description}>
|
||||
<article class="max-w-4xl mx-auto px-4 py-20">
|
||||
<BackToTop />
|
||||
<!-- Back button -->
|
||||
<div class="mb-8">
|
||||
<a href="/blog" class="btn btn-ghost btn-sm">
|
||||
<ChevronLeft size={18} />
|
||||
Back to Blog
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Featured Image -->
|
||||
{
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Post Header -->
|
||||
<header class="mb-8">
|
||||
<h1 class="text-5xl font-bold mb-4">{title}</h1>
|
||||
<p class="text-xl text-base-content/70 mb-4">{description}</p>
|
||||
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-4 text-sm text-base-content/60"
|
||||
>
|
||||
<time datetime={publishDate.toISOString()}>
|
||||
{formatDate(publishDate)}
|
||||
</time>
|
||||
<span>•</span>
|
||||
<span>{readingTime} min read</span>
|
||||
{
|
||||
updatedDate && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>Updated: {formatDate(updatedDate)}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
tags && tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-2 mt-4">
|
||||
{tags.map((tag) => (
|
||||
<TagBadge tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</header>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- TOC -->
|
||||
{
|
||||
toc.length > 0 && (
|
||||
<div class="collapse bg-base-200/50 rounded-xl mb-8 border border-base-300">
|
||||
<input type="checkbox" />
|
||||
|
||||
<p class="collapse-title font-bold uppercase text-xs tracking-widest opacity-60">
|
||||
Table of Contents
|
||||
</p>
|
||||
<div class="collapse-content text-sm">
|
||||
<ul class="space-y-3">
|
||||
{toc.map((item) => (
|
||||
<li
|
||||
class={`list-none ${item.depth === 3 ? "ml-6 text-sm" : "font-medium"}`}
|
||||
>
|
||||
<a
|
||||
href={`#${item.slug}`}
|
||||
class="hover:link transition-all flex items-center gap-2"
|
||||
>
|
||||
<span class="text-primary/40">{"#".repeat(item.depth)}</span>
|
||||
{item.text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="bg-base-200/50 ">
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Post Content -->
|
||||
<div
|
||||
class="max-w-none leading-7
|
||||
[&_h1]:text-4xl [&_h1]:font-bold [&_h1]:mt-8 [&_h1]:mb-4
|
||||
[&_h2]:text-3xl [&_h2]:font-bold [&_h2]:mt-8 [&_h2]:mb-4
|
||||
[&_h3]:text-2xl [&_h3]:font-bold [&_h3]:mt-8 [&_h3]:mb-4
|
||||
[&_h4]:text-xl [&_h4]:font-bold [&_h4]:mt-8 [&_h4]:mb-4
|
||||
[&_h5]:text-lg [&_h5]:font-bold [&_h5]:mt-8 [&_h5]:mb-4
|
||||
[&_h6]:text-base [&_h6]:font-bold [&_h6]:mt-8 [&_h6]:mb-4
|
||||
[&_p]:mb-4
|
||||
[&_a]:underline [&_a]:link
|
||||
[&_ul]:mb-4 [&_ul]:ml-6 [&_ul]:list-disc [&_ul]:list-outside
|
||||
[&_ol]:mb-4 [&_ol]:ml-6 [&_ol]:list-decimal [&_ol]:list-outside
|
||||
[&_li]:mb-2
|
||||
[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm [&_code]:font-mono [&_code]:bg-base-200
|
||||
[&_pre]:p-4 [&_pre]:rounded-lg [&_pre]:overflow-x-auto [&_pre]:mb-4 [&_pre]:bg-base-200
|
||||
[&_pre_code]:bg-transparent [&_pre_code]:p-0
|
||||
[&_blockquote]:border-l-4 [&_blockquote]:border-base-300 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:my-4
|
||||
[&_img]:rounded-lg [&_img]:my-6"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider mt-12"></div>
|
||||
|
||||
<!-- Back to blog link -->
|
||||
<div class="text-center mt-8">
|
||||
<a href="/blog" class="btn btn-primary"> View All Posts </a>
|
||||
</div>
|
||||
<div class="flex justify-center gap-2 mt-12">
|
||||
<div class="flex gap-3 justify-center flex-wrap text-sm">
|
||||
<a href="/blog" class="link link-hover">View All Posts</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>
|
||||
120
src/layouts/Layout.astro
Normal file
120
src/layouts/Layout.astro
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
import { ClientRouter } from "astro:transitions";
|
||||
import Oneko from "../components/Oneko.astro";
|
||||
import Console from "../components/Console.astro";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title = "Another Hadi",
|
||||
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.3s",
|
||||
easing: "ease-in-out",
|
||||
fillMode: "forwards",
|
||||
},
|
||||
new: {
|
||||
name: "blurFadeIn",
|
||||
duration: "0.3s",
|
||||
easing: "ease-in-out",
|
||||
fillMode: "backwards",
|
||||
},
|
||||
};
|
||||
|
||||
const pageTransition = {
|
||||
forwards: blurFadeAnimation,
|
||||
backwards: blurFadeAnimation,
|
||||
};
|
||||
|
||||
const origin = Astro.url.origin;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" transition:animate={pageTransition} data-theme="hadi">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<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`} />
|
||||
</head>
|
||||
<body class="min-h-screen">
|
||||
<slot />
|
||||
|
||||
<Oneko />
|
||||
<Console />
|
||||
|
||||
<!-- Smooth Scroll -->
|
||||
<style is:global>
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Initial Page Load Blur-Fade Animation */
|
||||
@keyframes pageLoadBlurFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
filter: blur(8px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
animation: pageLoadBlurFade 0.3s ease-in-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
/* Blur Fade View Transitions (for page-to-page navigation) */
|
||||
@keyframes blurFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
filter: blur(10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blurFadeOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
filter: blur(10px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
221
src/layouts/ProjectLayout.astro
Normal file
221
src/layouts/ProjectLayout.astro
Normal file
@@ -0,0 +1,221 @@
|
||||
---
|
||||
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">
|
||||
<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="text-center mt-8">
|
||||
<a href="/projects" class="btn btn-primary"> View All Projects </a>
|
||||
</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>
|
||||
Reference in New Issue
Block a user