mirror of
https://github.com/anotherhadi/blog.git
synced 2026-04-03 12:02:09 +02:00
init
This commit is contained in:
36
src/components/BackToTop.astro
Normal file
36
src/components/BackToTop.astro
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import { ArrowUp } from "lucide-astro";
|
||||
---
|
||||
|
||||
<button
|
||||
id="back-to-top"
|
||||
class="btn btn-circle btn-primary fixed bottom-6 right-6 z-50 transition-opacity duration-300 opacity-0 invisible"
|
||||
aria-label="Back to top"
|
||||
>
|
||||
<ArrowUp />
|
||||
</button>
|
||||
|
||||
<script>
|
||||
const backToTopBtn = document.getElementById("back-to-top");
|
||||
|
||||
if (backToTopBtn) {
|
||||
// Afficher/Masquer le bouton selon le scroll
|
||||
window.addEventListener("scroll", () => {
|
||||
if (window.scrollY > 300) {
|
||||
backToTopBtn.classList.remove("opacity-0", "invisible");
|
||||
backToTopBtn.classList.add("opacity-100", "visible");
|
||||
} else {
|
||||
backToTopBtn.classList.add("opacity-0", "invisible");
|
||||
backToTopBtn.classList.remove("opacity-100", "visible");
|
||||
}
|
||||
});
|
||||
|
||||
// Action de retour en haut
|
||||
backToTopBtn.addEventListener("click", () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
38
src/components/Blog.astro
Normal file
38
src/components/Blog.astro
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import BlogCard from "./BlogCard.astro";
|
||||
import { ArrowRight } from "@lucide/astro";
|
||||
|
||||
const blogEntries = await getCollection("blog");
|
||||
|
||||
// Sort by publish date, most recent first
|
||||
const sortedPosts = blogEntries.sort(
|
||||
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(),
|
||||
);
|
||||
|
||||
// Get only the latest 3 posts
|
||||
const latestPosts = sortedPosts.slice(0, 3);
|
||||
---
|
||||
|
||||
<section id="blog" class="py-20 px-4">
|
||||
<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} />)}
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-12">
|
||||
<a href="/blog" class="btn btn-ghost gap-2">
|
||||
View All Posts
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
57
src/components/BlogCard.astro
Normal file
57
src/components/BlogCard.astro
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import TagBadge from "./TagBadge.astro";
|
||||
import { Image } from "astro:assets";
|
||||
|
||||
interface Props {
|
||||
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"
|
||||
>
|
||||
<figure class="aspect-video">
|
||||
<Image
|
||||
src={post.data.image}
|
||||
alt={post.data.title}
|
||||
class="w-full h-full object-cover"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<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 && (
|
||||
<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">
|
||||
Read More
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
15
src/components/Console.astro
Normal file
15
src/components/Console.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
<script>
|
||||
function message() {
|
||||
const url = window.location.href;
|
||||
console.log(
|
||||
"Hey!\nWant to chat? Send me a dm on bsky (hadi1842.bsky.social) or a mail to anotherhadi.clapped234[at]passmail.net and I'll respond whenever I can.\nUse my gpg key (" +
|
||||
url +
|
||||
"anotherhadi.asc) for secure communication.",
|
||||
);
|
||||
}
|
||||
message();
|
||||
</script>
|
||||
23
src/components/Contact.astro
Normal file
23
src/components/Contact.astro
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import { Send } from "@lucide/astro";
|
||||
---
|
||||
|
||||
<section id="contact" class="py-20 px-4">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-4xl font-bold mb-4 flex gap-5 justify-center m-auto">
|
||||
Want to chat?
|
||||
</h2>
|
||||
<p class="text-lg text-base-content/70 max-w-2xl m-auto">
|
||||
Send me a dm on <a
|
||||
class="text-primary"
|
||||
href="https://bsky.app/profile/hadi1842.bsky.social">bsky</a
|
||||
> or a mail to <span class="text-primary"
|
||||
>anotherhadi.clapped234[at]passmail.net</span
|
||||
> and I'll respond whenever I can.
|
||||
<br />Use my <a class="text-primary" href="/anotherhadi.asc">gpg key</a>
|
||||
for secure communication.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
237
src/components/Hero.astro
Normal file
237
src/components/Hero.astro
Normal file
@@ -0,0 +1,237 @@
|
||||
---
|
||||
import { ArrowRight, Coffee, FolderCode, Key, Newspaper } from "@lucide/astro";
|
||||
import { Image } from "astro:assets";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
avatar: any;
|
||||
location?: string;
|
||||
socialLinks?: {
|
||||
github?: string;
|
||||
linkedin?: string;
|
||||
twitter?: string;
|
||||
bluesky?: string;
|
||||
instagram?: string;
|
||||
youTube?: string;
|
||||
medium?: string;
|
||||
kofi?: string;
|
||||
codetips?: string;
|
||||
};
|
||||
gpgKey?: string;
|
||||
}
|
||||
|
||||
const { name, title, description, avatar, location, socialLinks, gpgKey } =
|
||||
Astro.props;
|
||||
---
|
||||
|
||||
<section class="hero min-h-[65vh]">
|
||||
<div class="hero-content flex-col lg:flex-row-reverse max-w-7xl gap-8">
|
||||
<div class="avatar">
|
||||
<div
|
||||
class="w-48 ring-primary ring-offset-base-100 rounded-full ring-2 ring-offset-2"
|
||||
>
|
||||
<Image src={avatar} alt={name} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="text-5xl font-bold mb-4">
|
||||
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">
|
||||
{description}
|
||||
</p>
|
||||
{
|
||||
socialLinks && (
|
||||
<div class="flex gap-4">
|
||||
{socialLinks.github && (
|
||||
<div class="tooltip" data-tip="Github">
|
||||
<a
|
||||
href={socialLinks.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-circle btn-ghost"
|
||||
aria-label="GitHub"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{socialLinks.linkedin && (
|
||||
<div class="tooltip" data-tip="Linkedin">
|
||||
<a
|
||||
href={socialLinks.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-circle btn-ghost"
|
||||
aria-label="LinkedIn"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{socialLinks.twitter && (
|
||||
<div class="tooltip" data-tip="Twitter/X">
|
||||
<a
|
||||
href={socialLinks.twitter}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-circle btn-ghost"
|
||||
aria-label="X (Twitter)"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 32 32"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M18.42,14.009L27.891,3h-2.244l-8.224,9.559L10.855,3H3.28l9.932,14.455L3.28,29h2.244l8.684-10.095,6.936,10.095h7.576l-10.301-14.991h0Zm-3.074,3.573l-1.006-1.439L6.333,4.69h3.447l6.462,9.243,1.006,1.439,8.4,12.015h-3.447l-6.854-9.804h0Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{socialLinks.bluesky && (
|
||||
<div class="tooltip" data-tip="Bluesky">
|
||||
<a
|
||||
href={socialLinks.bluesky}
|
||||
class="btn btn-circle btn-ghost"
|
||||
aria-label="Bluesky"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 32 32"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M23.931,5.298c-3.21,2.418-6.663,7.32-7.931,9.951-1.267-2.631-4.721-7.533-7.931-9.951-2.316-1.744-6.069-3.094-6.069,1.201,0,.857,.49,7.206,.778,8.237,.999,3.583,4.641,4.497,7.881,3.944-5.663,.967-7.103,4.169-3.992,7.372,5.908,6.083,8.492-1.526,9.154-3.476,.123-.36,.179-.527,.179-.379,0-.148,.057,.019,.179,.379,.662,1.949,3.245,9.558,9.154,3.476,3.111-3.203,1.671-6.405-3.992-7.372,3.24,.553,6.882-.361,7.881-3.944,.288-1.031,.778-7.38,.778-8.237,0-4.295-3.753-2.945-6.069-1.201Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{socialLinks.instagram && (
|
||||
<div class="tooltip" data-tip="Instagram">
|
||||
<a
|
||||
href={`${socialLinks.instagram}`}
|
||||
class="btn btn-circle btn-ghost"
|
||||
aria-label="Instagram"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<path d="M10.202,2.098c-1.49,.07-2.507,.308-3.396,.657-.92,.359-1.7,.84-2.477,1.619-.776,.779-1.254,1.56-1.61,2.481-.345,.891-.578,1.909-.644,3.4-.066,1.49-.08,1.97-.073,5.771s.024,4.278,.096,5.772c.071,1.489,.308,2.506,.657,3.396,.359,.92,.84,1.7,1.619,2.477,.779,.776,1.559,1.253,2.483,1.61,.89,.344,1.909,.579,3.399,.644,1.49,.065,1.97,.08,5.771,.073,3.801-.007,4.279-.024,5.773-.095s2.505-.309,3.395-.657c.92-.36,1.701-.84,2.477-1.62s1.254-1.561,1.609-2.483c.345-.89,.579-1.909,.644-3.398,.065-1.494,.081-1.971,.073-5.773s-.024-4.278-.095-5.771-.308-2.507-.657-3.397c-.36-.92-.84-1.7-1.619-2.477s-1.561-1.254-2.483-1.609c-.891-.345-1.909-.58-3.399-.644s-1.97-.081-5.772-.074-4.278,.024-5.771,.096m.164,25.309c-1.365-.059-2.106-.286-2.6-.476-.654-.252-1.12-.557-1.612-1.044s-.795-.955-1.05-1.608c-.192-.494-.423-1.234-.487-2.599-.069-1.475-.084-1.918-.092-5.656s.006-4.18,.071-5.656c.058-1.364,.286-2.106,.476-2.6,.252-.655,.556-1.12,1.044-1.612s.955-.795,1.608-1.05c.493-.193,1.234-.422,2.598-.487,1.476-.07,1.919-.084,5.656-.092,3.737-.008,4.181,.006,5.658,.071,1.364,.059,2.106,.285,2.599,.476,.654,.252,1.12,.555,1.612,1.044s.795,.954,1.051,1.609c.193,.492,.422,1.232,.486,2.597,.07,1.476,.086,1.919,.093,5.656,.007,3.737-.006,4.181-.071,5.656-.06,1.365-.286,2.106-.476,2.601-.252,.654-.556,1.12-1.045,1.612s-.955,.795-1.608,1.05c-.493,.192-1.234,.422-2.597,.487-1.476,.069-1.919,.084-5.657,.092s-4.18-.007-5.656-.071M21.779,8.517c.002,.928,.755,1.679,1.683,1.677s1.679-.755,1.677-1.683c-.002-.928-.755-1.679-1.683-1.677,0,0,0,0,0,0-.928,.002-1.678,.755-1.677,1.683m-12.967,7.496c.008,3.97,3.232,7.182,7.202,7.174s7.183-3.232,7.176-7.202c-.008-3.97-3.233-7.183-7.203-7.175s-7.182,3.233-7.174,7.203m2.522-.005c-.005-2.577,2.08-4.671,4.658-4.676,2.577-.005,4.671,2.08,4.676,4.658,.005,2.577-2.08,4.671-4.658,4.676-2.577,.005-4.671-2.079-4.676-4.656h0" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{socialLinks.youTube && (
|
||||
<div class="tooltip" data-tip="Youtube">
|
||||
<a
|
||||
href={socialLinks.youTube}
|
||||
class="btn btn-circle btn-ghost"
|
||||
aria-label="YouTube"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 32 32"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M31.331,8.248c-.368-1.386-1.452-2.477-2.829-2.848-2.496-.673-12.502-.673-12.502-.673,0,0-10.007,0-12.502,.673-1.377,.37-2.461,1.462-2.829,2.848-.669,2.512-.669,7.752-.669,7.752,0,0,0,5.241,.669,7.752,.368,1.386,1.452,2.477,2.829,2.847,2.496,.673,12.502,.673,12.502,.673,0,0,10.007,0,12.502-.673,1.377-.37,2.461-1.462,2.829-2.847,.669-2.512,.669-7.752,.669-7.752,0,0,0-5.24-.669-7.752ZM12.727,20.758V11.242l8.364,4.758-8.364,4.758Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{socialLinks.medium && (
|
||||
<div class="tooltip" data-tip="Medium">
|
||||
<a
|
||||
href={socialLinks.medium}
|
||||
class="btn btn-circle btn-ghost"
|
||||
aria-label="Medium"
|
||||
target="_blank"
|
||||
>
|
||||
<Newspaper />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{socialLinks.kofi && (
|
||||
<div class="tooltip" data-tip="Ko-fi">
|
||||
<a
|
||||
href={socialLinks.kofi}
|
||||
class="btn btn-circle btn-ghost"
|
||||
aria-label="Ko-fi"
|
||||
target="_blank"
|
||||
>
|
||||
<Coffee />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{socialLinks.codetips && (
|
||||
<div class="tooltip" data-tip="Codetips">
|
||||
<a
|
||||
href={socialLinks.codetips}
|
||||
class="btn btn-circle btn-ghost"
|
||||
aria-label="CodeTips"
|
||||
target="_blank"
|
||||
>
|
||||
<FolderCode class="size-6" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{gpgKey && (
|
||||
<div class="tooltip" data-tip="Gpg key">
|
||||
<a
|
||||
href={gpgKey}
|
||||
class="btn btn-circle btn-ghost"
|
||||
aria-label="Gpg Key"
|
||||
target="_blank"
|
||||
>
|
||||
<Key class="size-6" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="mt-12 flex gap-5">
|
||||
<a href="/blog" class="btn btn-ghost gap-2">
|
||||
Blog
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
<a href="/projects" class="btn btn-ghost gap-2">
|
||||
Projects
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
22
src/components/Oneko.astro
Normal file
22
src/components/Oneko.astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
<script is:inline>
|
||||
function initOneko() {
|
||||
if (!document.getElementById("oneko")) {
|
||||
const script = document.createElement("script");
|
||||
script.src = "/scripts/oneko.js";
|
||||
script.id = "oneko-script";
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
}
|
||||
document.addEventListener("astro:page-load", initOneko);
|
||||
</script>
|
||||
|
||||
<style is:global>
|
||||
#oneko {
|
||||
z-index: 999;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
95
src/components/ProjectCard.astro
Normal file
95
src/components/ProjectCard.astro
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
import { Image } from "astro:assets";
|
||||
import TagBadge from "./TagBadge.astro";
|
||||
import { ExternalLink, Eye } from "@lucide/astro";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
interface Props {
|
||||
project: CollectionEntry<"projects">;
|
||||
}
|
||||
|
||||
const { project } = Astro.props;
|
||||
---
|
||||
|
||||
<article
|
||||
class="card bg-base-100 shadow-xl border border-base-200 rounded-lg hover:shadow-2xl transition-shadow"
|
||||
>
|
||||
<figure class="aspect-video">
|
||||
<Image
|
||||
src={project.data.image}
|
||||
alt={project.data.title}
|
||||
class="w-full h-full object-cover"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title hover:text-primary transition-colors">
|
||||
<a href={`/projects/${project.id}`}>{project.data.title}</a>
|
||||
</h2>
|
||||
<p class="text-base-content/80">{project.data.description}</p>
|
||||
{
|
||||
project.data.tags && project.data.tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
{project.data.tags.map((tag) => (
|
||||
<TagBadge tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div class="card-actions justify-end mt-4 gap-2">
|
||||
{
|
||||
project.data.demoLink && (
|
||||
<a
|
||||
href={project.data.demoLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-sm btn-ghost gap-1"
|
||||
>
|
||||
<ExternalLink class="size-4" />
|
||||
Demo
|
||||
</a>
|
||||
)
|
||||
}
|
||||
{
|
||||
project.data.url && (
|
||||
<a
|
||||
href={project.data.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-sm btn-ghost gap-1"
|
||||
>
|
||||
<ExternalLink class="size-4" />
|
||||
Website
|
||||
</a>
|
||||
)
|
||||
}
|
||||
{
|
||||
project.data.sourceLink && (
|
||||
<a
|
||||
href={project.data.sourceLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-sm btn-ghost gap-1"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
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>
|
||||
Source
|
||||
</a>
|
||||
)
|
||||
}
|
||||
<div class="tooltip" data-tip="View project details">
|
||||
<a href={`/projects/${project.id}`} class="btn btn-sm btn-primary">
|
||||
<Eye class="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
35
src/components/Projects.astro
Normal file
35
src/components/Projects.astro
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import ProjectCard from "./ProjectCard.astro";
|
||||
import { ArrowRight } from "@lucide/astro";
|
||||
|
||||
const projectEntries = await getCollection("projects");
|
||||
---
|
||||
|
||||
<section id="projects" class="py-20 px-4">
|
||||
<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. By creating these projects from scratch, I can ensure
|
||||
complete control over every aspect of their design and functionality.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{projectEntries.map((project) => <ProjectCard project={project} />)}
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-12">
|
||||
<a
|
||||
href="https://github.com/anotherhadi"
|
||||
target="_blank"
|
||||
class="btn btn-ghost gap-2"
|
||||
>
|
||||
View All Projects
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
9
src/components/TagBadge.astro
Normal file
9
src/components/TagBadge.astro
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
interface Props {
|
||||
tag: string;
|
||||
}
|
||||
|
||||
const { tag } = Astro.props;
|
||||
---
|
||||
|
||||
<span class="badge badge-sm rounded-sm badge-soft badge-accent">{tag}</span>
|
||||
Reference in New Issue
Block a user