first commit
This commit is contained in:
46
src/components/layout/Footer.astro
Normal file
46
src/components/layout/Footer.astro
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
import Social from "../ui/Social.astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
const name = "EFEELE";
|
||||
const email = "hello@efeele.dev";
|
||||
const github = "https://github.com/EFEELE";
|
||||
const linkedin = "https://www.linkedin.com/in/efeele/";
|
||||
const instagram = "https://www.instagram.com/efeele.dev/";
|
||||
const youtube = "https://www.youtube.com/@efeeledev";
|
||||
---
|
||||
|
||||
<footer
|
||||
class="relative bottom-0 w-full px-4 py-8 font-medium text-blacktext dark:bg-transparent dark:border-b-2 dark:border-zinc-800 dark:text-zinc-300 max-lg:mt-3"
|
||||
role="contentinfo"
|
||||
aria-label="Site footer"
|
||||
>
|
||||
<nav
|
||||
class="mx-auto flex max-w-7xl flex-row items-center justify-between gap-4 text-xl max-xl:px-6 max-sm:flex-col"
|
||||
aria-label="Footer navigation"
|
||||
>
|
||||
<div
|
||||
class="relative h-6 cursor-pointer before:absolute before:left-1/2 before:top-1/2 before:h-full before:w-[40%] before:-translate-x-1/2 before:-translate-y-1/2 before:rounded-full before:bg-[#50fd8f25] before:blur-3xl before:opacity-80 before:-z-1 hover:text-mint-500 transition-all [text-shadow:_0_1px_2px_#000]"
|
||||
>
|
||||
<a href="/" aria-label="Return to homepage">
|
||||
<!-- This icon represents the logo -->
|
||||
<Icon name="logo" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a
|
||||
href="https://github.com/EFEELE"
|
||||
class="flex items-center justify-center gap-3 text-base font-normal italic max-sm:text-sm"
|
||||
aria-label="About the website development"
|
||||
><Icon name="code" aria-hidden="true" /> Developed by <strong>EFEELE</strong>, with Astro</a
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-5" role="list" aria-label="Social media links">
|
||||
<Social link={email} iconName="envelope" label={`Send email to ${email}`} />
|
||||
<Social link={instagram} iconName="instagram" label={`Visit ${name} on Instagram`} />
|
||||
<Social link={youtube} iconName="youtube" label={`Visit ${name} on YouTube`} />
|
||||
<Social link={github} iconName="github" label={`Visit ${name} on GitHub`} />
|
||||
<Social link={linkedin} iconName="linkedin" label={`Visit ${name} on LinkedIn`} />
|
||||
</div>
|
||||
</nav>
|
||||
</footer>
|
||||
61
src/components/layout/Header.astro
Normal file
61
src/components/layout/Header.astro
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
import Navigation from "./Navigation.astro";
|
||||
import ThemeIcon from "../ui/ThemeIcon.astro";
|
||||
import Social from "../ui/Social.astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
const currentPath = Astro.url.pathname;
|
||||
const routes = ["/", "/portfolio", "/about-me", ];
|
||||
|
||||
const github = "https://github.com/EFEELE";
|
||||
const linkedin = "https://www.linkedin.com/in/efeele/";
|
||||
|
||||
// Check if the current route is in the list of routes
|
||||
const isActiveRoute = routes.includes(currentPath);
|
||||
|
||||
const navItems = isActiveRoute
|
||||
? ["home", "experience", "projects", "about", "blog",]
|
||||
: ["home", "blog", "about"]; // Change the items
|
||||
---
|
||||
|
||||
<header
|
||||
role="banner"
|
||||
aria-label="Main navigation"
|
||||
class="sticky top-0 z-50 w-full p-4 font-medium text-blacktext dark:text-zinc-300 dark:bg-[#0E0E11]/80 dark:border-b-1 dark:border-zinc-800 bg-white/90 backdrop-blur-xs dark:backdrop-blur-xs max-md:z-50 max-md:px-0 transition-all"
|
||||
>
|
||||
<div
|
||||
class="relative mx-auto flex max-w-7xl flex-row items-center justify-between max-xl:px-6"
|
||||
>
|
||||
<a href="/" aria-label="Go to home">
|
||||
<Icon
|
||||
name="logo"
|
||||
class="h-6 cursor-pointer transition-all hover:text-mint-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<Navigation items={navItems} />
|
||||
|
||||
<div class="flex items-center justify-between gap-5 text-xl">
|
||||
<div class="max-md:hidden flex items-center justify-center gap-5" role="list">
|
||||
<Social link={github} iconName="github" />
|
||||
<Social
|
||||
link={linkedin}
|
||||
iconName="linkedin"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-5 text-xl md:pl-5">
|
||||
<ThemeIcon />
|
||||
<button
|
||||
class="hamburger"
|
||||
aria-label="Open menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="mobile-menu"
|
||||
>
|
||||
<Icon name="bars" class="hamburger-icon bars-icon" aria-hidden="true" />
|
||||
<Icon name="xmark" class="hamburger-icon xmark-icon" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
92
src/components/layout/NavArticle.astro
Normal file
92
src/components/layout/NavArticle.astro
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="max-xl:hidden ">
|
||||
<div id="nav-content" class="bg-white dark:bg-transparent sticky w-72 mt-8 rounded-2xl dark:border-0 border border-neutral-100 top-14 max-h-[calc(100svh-3.5rem)] overflow-x-hidden px-6 pt-8 pb-12">
|
||||
<div class="flex flex-col gap-4 pl-0">
|
||||
<div>
|
||||
<h3 class="dark:text-zinc-400 text-blacktext/90 font-black tracking-wide text-md uppercase">Table of Contents</h3>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 pr-6 text-neutral-500 dark:text-neutral-300 ">
|
||||
<ul id="toc-list" class="leading-loose text-base gap-2 border-l dark:border-neutral-500/20 border-blacktext/20">
|
||||
<li class="leading-loose">
|
||||
<a class="inline-block leading-5 pl-4 font-bold text-white border-l dark:border-white border-blacktext dark:hover:border-white hover:border-blacktext" href="#">{title}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const tocList = document.getElementById("toc-list");
|
||||
const content = document.getElementById("content");
|
||||
|
||||
if (!tocList || !content) return;
|
||||
|
||||
const headers = content.querySelectorAll("h2, h3");
|
||||
let currentUl = tocList;
|
||||
|
||||
headers.forEach((header, index) => {
|
||||
if (!header.id) {
|
||||
header.id = header.textContent?.trim().toLowerCase().replace(/\s+/g, "-") + "-" + index;
|
||||
// 👈 Add the class
|
||||
}
|
||||
|
||||
const li = document.createElement("li");
|
||||
const link = document.createElement("a");
|
||||
link.href = `#${header.id}`;
|
||||
link.textContent = header.textContent?.trim() || header.id;
|
||||
|
||||
// If the header is H2, keep pl-6, and if it's H3, use pl-12
|
||||
link.classList.add("inline-block","leading-5", "hover:text-mint-400", "py-2", "border-l", "border-transparent", "dark:hover:border-white","hover:border-blacktext");
|
||||
link.classList.add(header.tagName === "H2" ? "pl-6" : "pl-12");
|
||||
console.log("classes removed 2");
|
||||
li.appendChild(link);
|
||||
|
||||
if (header.tagName === "H2") {
|
||||
currentUl = document.createElement("ul");
|
||||
currentUl.classList.add("border-neutral-400","dark:hover:border-white","hover:border-blacktext", "pl-0");
|
||||
console.log("classes removed 3");
|
||||
const h2Li = document.createElement("li");
|
||||
h2Li.appendChild(link);
|
||||
h2Li.appendChild(currentUl);
|
||||
tocList.appendChild(h2Li);
|
||||
} else {
|
||||
currentUl.appendChild(li);
|
||||
}
|
||||
|
||||
// Smooth scroll when clicking on a link
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
document.getElementById(header.id)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
});
|
||||
});
|
||||
|
||||
// 👇 Detect the active header
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const id = entry.target.getAttribute("id");
|
||||
const link = document.querySelector(`a[href="#${id}"]`);
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
// Remove class from all and add only to the active one
|
||||
document.querySelectorAll("#toc-list a").forEach((el) => {
|
||||
el.classList.remove("font-semibold", "dark:!text-mint-300", "!text-blacktext", "dark:!border-white", "!border-blacktext" );
|
||||
el.classList.add("dark:text-neutral-300","text-neutral-500" );
|
||||
console.log("classes removed 4");
|
||||
});
|
||||
|
||||
link?.classList.add("font-semibold", "dark:!text-mint-300", "!text-blacktext", "border-l", "dark:!border-white", "!border-blacktext");
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "-30% 0px -65% 0px", threshold: 0.1 } // Adjusted to improve visibility
|
||||
);
|
||||
|
||||
headers.forEach((header) => observer.observe(header));
|
||||
});
|
||||
</script>
|
||||
160
src/components/layout/Navigation.astro
Normal file
160
src/components/layout/Navigation.astro
Normal file
@@ -0,0 +1,160 @@
|
||||
---
|
||||
import Social from "../ui/Social.astro";
|
||||
|
||||
const { items = [] }: { items: (keyof typeof menu)[] } = Astro.props as {
|
||||
items: (keyof typeof menu)[];
|
||||
};
|
||||
|
||||
const menu = {
|
||||
about: { name: "About Me", path: "/about-me" },
|
||||
blog: {
|
||||
name: "Blog",
|
||||
path: "/blog/",
|
||||
dropdown: [
|
||||
{ name: "All Posts", path: "/blog/all-posts" },
|
||||
]
|
||||
},
|
||||
home: { name: "Home", path: "/#home" },
|
||||
experience: { name: "Experience", path: "/#experience" },
|
||||
projects: { name: "Projects", path: "/#projects" },
|
||||
};
|
||||
|
||||
// Common base classes
|
||||
const baseClasses = {
|
||||
nav: "nav-links flex w-full justify-center gap-6 max-md:gap-3 max-md:py-6",
|
||||
link: "px-2 py-2 transition-all hover:text-mint-300 max-md:mx-auto max-md:w-full max-md:px-6 max-md:py-2 ",
|
||||
socialContainer: "flex items-center justify-center gap-5 md:hidden",
|
||||
dropdown: "relative group flex items-center",
|
||||
dropdownMenu: "absolute left-0 top-full hidden group-hover:block bg-white dark:bg-zinc-800 shadow-lg rounded-md py-2 min-w-[200px] z-50",
|
||||
dropdownItem: "block px-4 py-2 text-sm hover:bg-mint-100 dark:hover:bg-zinc-700 transition-colors"
|
||||
} as const;
|
||||
---
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const isHome = window.location.pathname === "/";
|
||||
|
||||
// Classes for link states
|
||||
const linkClasses = {
|
||||
active: ["text-mint-500","dark:text-mint-400", "font-bold", "[text-shadow:_1px_1px_11px_rgba(208,251,229,0.7)]"],
|
||||
inactive: ["dark:text-zinc-300", "text-blacktext"]
|
||||
};
|
||||
|
||||
function toggleLinkClasses(link: Element, isActive: boolean) {
|
||||
if (isActive) {
|
||||
link.classList.add(...linkClasses.active);
|
||||
link.classList.remove(...linkClasses.inactive);
|
||||
link.setAttribute('aria-current', 'page');
|
||||
} else {
|
||||
link.classList.remove(...linkClasses.active);
|
||||
link.classList.add(...linkClasses.inactive);
|
||||
link.removeAttribute('aria-current');
|
||||
}
|
||||
}
|
||||
|
||||
function updateActiveLink() {
|
||||
const currentPath = window.location.pathname;
|
||||
const currentHash = window.location.hash ? `#${window.location.hash.substring(1)}` : "";
|
||||
|
||||
document.querySelectorAll("nav a").forEach((link) => {
|
||||
const path = link.getAttribute("data-path");
|
||||
toggleLinkClasses(link, path === currentPath || path === currentHash);
|
||||
});
|
||||
}
|
||||
|
||||
function setupScrollSpy() {
|
||||
if (!isHome) return;
|
||||
|
||||
const sections = document.querySelectorAll("section[id]");
|
||||
const navLinks = document.querySelectorAll("nav a");
|
||||
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: "-50% 0px",
|
||||
threshold: 0
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const sectionId = entry.target.getAttribute("id");
|
||||
if (sectionId) {
|
||||
navLinks.forEach(link => {
|
||||
const path = link.getAttribute("data-path");
|
||||
toggleLinkClasses(link, path === `/#${sectionId}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
sections.forEach(section => observer.observe(section));
|
||||
}
|
||||
|
||||
updateActiveLink();
|
||||
setupScrollSpy();
|
||||
window.addEventListener("hashchange", updateActiveLink);
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav
|
||||
class={baseClasses.nav}
|
||||
role="navigation"
|
||||
aria-label="Main Navigation"
|
||||
>
|
||||
{
|
||||
items.map((key: string) => {
|
||||
const item = menu[key as keyof typeof menu];
|
||||
if (!item) return null;
|
||||
|
||||
if ('dropdown' in item) {
|
||||
return (
|
||||
<div class={baseClasses.dropdown}>
|
||||
<a
|
||||
href={item.path}
|
||||
class={baseClasses.link}
|
||||
data-path={item.path}
|
||||
aria-label={item.name}
|
||||
aria-current={item.path === Astro.url.pathname ? 'page' : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
<div class={baseClasses.dropdownMenu}>
|
||||
{item.dropdown.map((dropdownItem) => (
|
||||
<a
|
||||
href={dropdownItem.path}
|
||||
class={baseClasses.dropdownItem}
|
||||
data-path={dropdownItem.path}
|
||||
aria-label={dropdownItem.name}
|
||||
>
|
||||
{dropdownItem.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={item.path}
|
||||
class={baseClasses.link}
|
||||
data-path={item.path}
|
||||
aria-label={item.name}
|
||||
aria-current={item.path === Astro.url.pathname ? 'page' : undefined}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<div
|
||||
class={baseClasses.socialContainer}
|
||||
role="group"
|
||||
aria-label="Social Media Links"
|
||||
>
|
||||
<Social link="https://github.com/EFEELE" iconName="github" />
|
||||
<Social link="https://www.linkedin.com/in/efeele/" iconName="linkedin" />
|
||||
</div>
|
||||
</nav>
|
||||
42
src/components/layout/NavigationArticles.astro
Normal file
42
src/components/layout/NavigationArticles.astro
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
const allPosts = await Astro.glob("../../pages/blog/posts/*.md");
|
||||
|
||||
|
||||
// Ensure posts have a date before sorting them
|
||||
const sortedPosts = allPosts
|
||||
.filter(post => post.frontmatter.pubDate)
|
||||
.sort((a, b) => new Date(b.frontmatter.pubDate).getTime() - new Date(a.frontmatter.pubDate).getTime());
|
||||
|
||||
const currentSlug = Astro.url.pathname.split("/").filter(Boolean).pop();
|
||||
const currentIndex = sortedPosts.findIndex((post) =>
|
||||
post.url && post.url.includes(currentSlug || "")
|
||||
);
|
||||
const nextPost = currentIndex > 0 ? sortedPosts[currentIndex - 1] : null;
|
||||
const prevPost = currentIndex < sortedPosts.length - 1 ? sortedPosts[currentIndex + 1] : null;
|
||||
|
||||
console.log({ prevPost, nextPost });
|
||||
---
|
||||
|
||||
<nav class="mt-8 flex flex-row gap-2 w-full p-6 max-xl:p-3 max-lg:p-2">
|
||||
{prevPost && (
|
||||
<a
|
||||
href={prevPost.url}
|
||||
style="width: -webkit-fill-available;"
|
||||
class="relative flex min-w-1/2 items-center justify-start gap-2 font-semibold dark:text-white text-blacktext text-left text-pretty max-sm:text-xs max-md:text-sm max-md:leading-4 hover:text-mint-300 hover:[text-shadow:_1px_1px_11px_rgba(208,251,229,0.7)] transition-all before:absolute before:-top-5 before:left-0 before:text-sm before:font-light before:content-['Previous_Post']"
|
||||
>
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.85355 3.85355C7.04882 3.65829 7.04882 3.34171 6.85355 3.14645C6.65829 2.95118 6.34171 2.95118 6.14645 3.14645L2.14645 7.14645C1.95118 7.34171 1.95118 7.65829 2.14645 7.85355L6.14645 11.8536C6.34171 12.0488 6.65829 12.0488 6.85355 11.8536C7.04882 11.6583 7.04882 11.3417 6.85355 11.1464L3.20711 7.5L6.85355 3.85355ZM12.8536 3.85355C13.0488 3.65829 13.0488 3.34171 12.8536 3.14645C12.6583 2.95118 12.3417 2.95118 12.1464 3.14645L8.14645 7.14645C7.95118 7.34171 7.95118 7.65829 8.14645 7.85355L12.1464 11.8536C12.3417 12.0488 12.6583 12.0488 12.8536 11.8536C13.0488 11.6583 13.0488 11.3417 12.8536 11.1464L9.20711 7.5L12.8536 3.85355Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
{prevPost.frontmatter.title}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{nextPost && (
|
||||
<a
|
||||
href={nextPost.url}
|
||||
style="width: -webkit-fill-available;"
|
||||
class="relative flex min-w-1/2 items-center justify-end gap-2 font-semibold dark:text-white text-blacktext text-right text-pretty max-sm:text-xs max-md:text-sm max-md:leading-4 hover:text-mint-300 hover:[text-shadow:_1px_1px_11px_rgba(208,251,229,0.7)] transition-all before:absolute before:-top-5 before:right-0 before:text-sm before:font-light before:content-['Next_Post']"
|
||||
>
|
||||
{nextPost.frontmatter.title}
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.14645 11.1464C1.95118 11.3417 1.95118 11.6583 2.14645 11.8536C2.34171 12.0488 2.65829 12.0488 2.85355 11.8536L6.85355 7.85355C7.04882 7.65829 7.04882 7.34171 6.85355 7.14645L2.85355 3.14645C2.65829 2.95118 2.34171 2.95118 2.14645 3.14645C1.95118 3.34171 1.95118 3.65829 2.14645 3.85355L5.79289 7.5L2.14645 11.1464ZM8.14645 11.1464C7.95118 11.3417 7.95118 11.6583 8.14645 11.8536C8.34171 12.0488 8.65829 12.0488 8.85355 11.8536L12.8536 7.85355C13.0488 7.65829 13.0488 7.34171 12.8536 7.14645L8.85355 3.14645C8.65829 2.95118 8.34171 2.95118 8.14645 3.14645C7.95118 3.34171 7.95118 3.65829 8.14645 3.85355L11.7929 7.5L8.14645 11.1464Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
</a>
|
||||
)}
|
||||
</nav>
|
||||
Reference in New Issue
Block a user