first commit

This commit is contained in:
EFEELE
2025-04-07 15:50:13 -06:00
commit c2421d79c5
124 changed files with 12129 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
---
const { title, url, date, image, tags = [], languages = [] } = Astro.props;
import Tag from "../ui/Tag.astro";
import ReadMore from "../ui/ReadMore.astro";
import DatePub from "./DatePub.astro";
import Capsule from "../ui/Capsule.astro";
---
<article class="bg-white dark:bg-zinc-900/25 dark:border dark:border-zinc-800 dark:hover:border-mint-300 backdrop-blur-lg shadow-sm overflow-auto hover:shadow-[5px_5px_rgba(0,_98,_90,_0.4),_10px_10px_rgba(0,_98,_90,_0.3),_15px_15px_rgba(0,_98,_90,_0.2),_20px_20px_rgba(0,_98,_90,_0.1),_25px_25px_rgba(0,_98,_90,_0.05)] p-8 max-md:p-6 w-full flex justify-between items-center bg-gradient-to-r hover:from-teal-200 hover:to-emerald-200 dark:hover:from-riptide-500 dark:hover:to-mint-500 transition-all hover:scale-105 duration-300 ease-in-out gap-8 max-md:gap-4 rounded-3xl max-md:flex-col-reverse">
<div class="flex flex-col">
<a href={url} class="flex flex-col gap-4 w-full">
<DatePub date={date} />
<h2 class="dark:text-mint-50 text-blacktext text-3xl font-bold text-pretty">{title}</h2>
<ReadMore />
</a>
<div class="gap-3 mt-3 flex flex-col">
<div class="flex gap-2 flex-wrap">
{languages.length > 0 && languages.map((language: string) => <Capsule lang={language} />)}
</div>
<div class="gap-2 flex flex-wrap justify-start items-center">
{tags.length > 0 && tags.map((tag: string) => <Tag tag={tag}>{tag}</Tag>)}
</div>
</div>
</div>
{image?.url && (
<a href={url} style={{ backgroundImage: `url(${image.url})` }} class="flex-shrink-0 rounded-2xl bg-center bg-cover aspect-square max-md:aspect-video w-1/6 max-md:w-full" />
)}
</article>

View File

@@ -0,0 +1,28 @@
---
const {date, class: className} = Astro.props;
---
<span
class={`flex flex-row center text-sm font-semibold items-center gap-3 text-blacktext dark:text-riptide-50 ${className || ''}`}
>
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
><path
d="M4.5 1C4.77614 1 5 1.22386 5 1.5V2H10V1.5C10 1.22386 10.2239 1 10.5 1C10.7761 1 11 1.22386 11 1.5V2H12.5C13.3284 2 14 2.67157 14 3.5V12.5C14 13.3284 13.3284 14 12.5 14H2.5C1.67157 14 1 13.3284 1 12.5V3.5C1 2.67157 1.67157 2 2.5 2H4V1.5C4 1.22386 4.22386 1 4.5 1ZM10 3V3.5C10 3.77614 10.2239 4 10.5 4C10.7761 4 11 3.77614 11 3.5V3H12.5C12.7761 3 13 3.22386 13 3.5V5H2V3.5C2 3.22386 2.22386 3 2.5 3H4V3.5C4 3.77614 4.22386 4 4.5 4C4.77614 4 5 3.77614 5 3.5V3H10ZM2 6V12.5C2 12.7761 2.22386 13 2.5 13H12.5C12.7761 13 13 12.7761 13 12.5V6H2ZM7 7.5C7 7.22386 7.22386 7 7.5 7C7.77614 7 8 7.22386 8 7.5C8 7.77614 7.77614 8 7.5 8C7.22386 8 7 7.77614 7 7.5ZM9.5 7C9.22386 7 9 7.22386 9 7.5C9 7.77614 9.22386 8 9.5 8C9.77614 8 10 7.77614 10 7.5C10 7.22386 9.77614 7 9.5 7ZM11 7.5C11 7.22386 11.2239 7 11.5 7C11.7761 7 12 7.22386 12 7.5C12 7.77614 11.7761 8 11.5 8C11.2239 8 11 7.77614 11 7.5ZM11.5 9C11.2239 9 11 9.22386 11 9.5C11 9.77614 11.2239 10 11.5 10C11.7761 10 12 9.77614 12 9.5C12 9.22386 11.7761 9 11.5 9ZM9 9.5C9 9.22386 9.22386 9 9.5 9C9.77614 9 10 9.22386 10 9.5C10 9.77614 9.77614 10 9.5 10C9.22386 10 9 9.77614 9 9.5ZM7.5 9C7.22386 9 7 9.22386 7 9.5C7 9.77614 7.22386 10 7.5 10C7.77614 10 8 9.77614 8 9.5C8 9.22386 7.77614 9 7.5 9ZM5 9.5C5 9.22386 5.22386 9 5.5 9C5.77614 9 6 9.22386 6 9.5C6 9.77614 5.77614 10 5.5 10C5.22386 10 5 9.77614 5 9.5ZM3.5 9C3.22386 9 3 9.22386 3 9.5C3 9.77614 3.22386 10 3.5 10C3.77614 10 4 9.77614 4 9.5C4 9.22386 3.77614 9 3.5 9ZM3 11.5C3 11.2239 3.22386 11 3.5 11C3.77614 11 4 11.2239 4 11.5C4 11.7761 3.77614 12 3.5 12C3.22386 12 3 11.7761 3 11.5ZM5.5 11C5.22386 11 5 11.2239 5 11.5C5 11.7761 5.22386 12 5.5 12C5.77614 12 6 11.7761 6 11.5C6 11.2239 5.77614 11 5.5 11ZM7 11.5C7 11.2239 7.22386 11 7.5 11C7.77614 11 8 11.2239 8 11.5C8 11.7761 7.77614 12 7.5 12C7.22386 12 7 11.7761 7 11.5ZM9.5 11C9.22386 11 9 11.2239 9 11.5C9 11.7761 9.22386 12 9.5 12C9.77614 12 10 11.7761 10 11.5C10 11.2239 9.77614 11 9.5 11Z"
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"></path></svg
>
{
new Date(date).toLocaleDateString("en-US", {
timeZone: "UTC", // Avoid timezone adjustments
day: "numeric",
month: "long",
year: "numeric",
})
}</span
>

View File

@@ -0,0 +1,106 @@
---
import Button from "../ui/Button.astro";
import LastPost from "../blog/LastPost.astro";
import Languages from "../blog/Languages.astro";
import { Icon } from "astro-icon/components";
import Heading from "../ui/Heading.astro";
const {
profileImage = "/images/efeeleprofile.webp",
profileAlt = "Photo of Fernando Aldair López Ponce (EFEELE) for the blog",
profileLink = "/about-me",
profileTitle = "FrontEnd Developer",
profileName = "Fernando Aldair López Ponce",
githubLink = "https://github.com/EFEELE/NeonMint",
githubText = "Do you like this blog's design?",
techsTitle = "TECHS"
} = Astro.props;
---
<section class="py-8 px-8 max-sm:px-4 max-sm:py-2">
<div
class="grid md:grid-cols-4 md:grid-rows-2 gap-4 max-sm:gap-3 grid-cols-2 grid-rows-4 max-w-7xl mx-auto max-md:h-[80vh] max-sm:h-auto max-sm:grid-rows-[auto_200px_auto_auto] max-xl:h-[550px] xl:h-[700px]"
>
<div
class="p-8 max-md:gap-0 max-xl:p-5 h-full max-md:p-4 max-lg:gap-1 gap-3 flex flex-col border border-emerald-50 rounded-2xl dark:bg-zinc-800 bg-gradient-to-r from-riptide-200 to-mint-200 dark:from-riptide-500 dark:to-mint-500 dark:border-zinc-800 dark:border-2 dark:bg-gradient-radial md:col-start-4 col-start-2 row-start-2"
>
<Heading text={techsTitle} textGradient="" level={3}/>
<div class="overflow-y-auto">
<Languages />
</div>
</div>
<div
class="group hover:shadow-[0_20px_50px_rgba(13,_188,_130,_0.4)] hover:scale-105 z-40 rounded-2xl transition-all ease-in duration-150 col-start-1 row-start-2 md:col-start-3 bg-gradient-to-r from-mint-300 dark:from-mint-600 to-mint-50 dark:to-mint-200/5 hover:to-mint-300/30 dark:hover:to-mint-600/30 p-[.2rem] "
>
<div class="w-full h-full overflow-hidden rounded-2xl dark:bg-zinc-900">
<div
class="w-full h-full relative overflow-hidden rounded-2xl bg-gradient-to-tr from-riptide-100 to-white dark:from-transparent dark:bg-gradient-to-bl dark:to-transparent "
>
<a target="_blank" href={githubLink}>
<div
class="p-8 h-full relative max-xl:p-5 max-md:p-4 flex flex-col gap-8 dark:before:[background-image:radial-gradient(circle,_rgba(13,188,130)_0,_rgba(1,45,34)_100%)] before:[background-image:radial-gradient(circle,_rgba(95,255,202)_0,_rgba(144,253,210)_100%)] before:h-[30%] before:absolute before:aspect-square xl:before:bottom-10 before:left-30 before:bottom-30 before:rounded-full before:blur-3xl before:opacity-90 before:transition before:z-0"
>
<div class="z-2">
<Heading
text={githubText}
textGradient=""
level={3}
/>
</div>
<Icon
class="group-hover:animate-pulse ease-in-out size-50 absolute -bottom-10 xl:size-56 xl:-bottom-5 left-30 max-sm:left-10 max-md:left-30 xl:-right-24 text-mint-500/30"
name="github"
/>
</div>
</a>
</div>
</div>
</div>
<div
class="flex col-span-2 col-start-1 row-start-1 md:col-start-3 bg-gradient-to-r rounded-2xl from-mint-300 dark:from-mint-600 to-mint-50 dark:to-mint-200/5 hover:to-mint-300/30 dark:hover:to-mint-600/30 p-[.2rem] "
>
<article
class="group hover:shadow-[0_10px_50px_rgba(13,_188,_130,_0.2)] w-full h-full overflow-hidden p-4 max-md:p-4 gap-4 max-md:gap-1 max-lg:gap-0 rounded-2xl bg-gradient-to-tr from-riptide-100 to-mint-50 flex dark:bg-gradient-to-r z-0 dark:overflow-hidden relative dark:from-mint-900 dark:to-mint-950 dark:before:[background-image:radial-gradient(circle,_rgba(13,188,130)_0,_rgba(1,45,34)_100%)] before:[background-image:radial-gradient(circle,_rgba(95,255,202)_0,_rgba(144,253,210)_100%)] dark:before:h-[80%] before:h-[30%] before:absolute before:aspect-square dark:before:left-30 before:left-50 dark:before:-bottom-0 before:bottom-40 before:rounded-full dark:before:blur-3xl before:blur-2xl dark:before:opacity-80 before:opacity-100 before:-z-10 before:transition"
>
<div
class="group-hover:scale-103 ease-in-out duration-500 w-5/12 max-sm:w-auto flex justify-center items-center"
>
<div
aria-label={profileAlt}
class="h-full w-full max-sm:rounded-full rounded-2xl max-sm:size-20 bg-center bg-cover"
style={`background-image: url(${profileImage})`}
>
<a href={profileLink} class="h-full w-full flex"></a>
</div>
</div>
<div class="p-4 w-7/12 flex flex-col justify-center gap-4 max-sm:w-fit max-md:gap-2">
<span
class="font-extrabold text-lg max-xl:text-base max-lg:text-sm max-lg:flex max-lg:flex-col-reverse max-md:flex-row leading-normal max-sm:leading-none"
><b
class="bg-gradient-to-r from-riptide-400 to-mint-400 dark:from-riptide-200 dark:to-mint-400 text-transparent bg-clip-text"
>{profileTitle}</b
> 🚀</span
>
<Heading
text={profileName}
level={3}
/>
<Button
link={profileLink}
text="About Me"
iconName="person"
class="drop-shadow-xl"
/>
</div>
</article>
</div>
<div class="col-span-2 row-span-2 md:col-start-1 md:row-start-1 bg-conic/[from_var(--border-angle)] dark:from-mint-200/30 dark:via-mint-500 dark:to-mint-200/20 from-mint-300/30 via-mint-500 to-mint-300/20 from-20% to-80% animate-rotate-border rounded-2xl p-[.2rem]">
<LastPost />
</div>
</div>
</section>

View File

@@ -0,0 +1,27 @@
---
const allPosts = await Astro.glob("../../pages/blog/posts/*.md");
const languages = [
...new Set(allPosts.map((post) => post.frontmatter.languages).flat()),
];
import Capsule from "../ui/Capsule.astro";
const { variant = "default" } = Astro.props;
const baseClasses = "flex flex-wrap";
const variantClasses = {
default: "gap-3 max-lg:gap-1",
vertical: "gap-6 flex-col"
} as const;
const classes = `${baseClasses} ${variantClasses[variant as keyof typeof variantClasses] || variantClasses.default}`;
---
<div class={classes}>
{languages.map((language) => (
<Capsule lang={language} linkEnabled={true} size="md" />
))}
</div>

View File

@@ -0,0 +1,83 @@
---
const allPosts = await Astro.glob("../../pages/blog/posts/*.md");
import Tag from "../ui/Tag.astro";
import ReadMore from "../ui/ReadMore.astro";
import Capsule from "../ui/Capsule.astro";
import DatePub from "./DatePub.astro";
// Get the latest post using reduce
const latestPost = allPosts.reduce((latest, current) => {
const latestDate = new Date(latest.frontmatter.pubDate).getTime();
const currentDate = new Date(current.frontmatter.pubDate).getTime();
return currentDate > latestDate ? current : latest;
});
const tags = [...new Set(latestPost.frontmatter.tags ?? [])];
const languages = [...new Set(latestPost.frontmatter.languages ?? [])];
const image = latestPost.frontmatter.image.url;
const imageAlt =
latestPost.frontmatter.image.alt || latestPost.frontmatter.title;
---
{
latestPost && (
<div
style={{ backgroundImage: `url(${image})` }}
class="h-full hover:shadow-[0_20px_50px_rgba(13,_188,_130,_0.2)] flex flex-col overflow-hidden rounded-2xl bg-gradient-to-br bg-center bg-cover transition-all ease-in-out duration-200"
role="article"
aria-labelledby="post-title"
>
<article class="h-full flex flex-col justify-between max-sm:bg-zinc-900 max-sm:relative min-sm:bg-gradient-to-t from-black/95 from-25% to-transparent max-sm:from-60% p-8 max-md:p-6 max-sm:mp-0 max-sm:p-0">
<a
href=""
class="min-sm:hidden relative top-0 left-0 w-full h-auto -z-0"
aria-hidden="true"
>
<img
src={image}
alt={imageAlt}
class="w-full h-auto"
loading="lazy"
/>
</a>
<div
class="w-full flex pb-5 gap-2 flex-wrap justify-end z-10 max-sm:px-6 max-sm:pt-6"
role="list"
aria-label="Programming languages"
>
{languages.map((language: unknown) => (
<Capsule lang={language?.toString() || ""} />
))}
</div>
<a
href={latestPost.url}
class="text-mint-50 gap-3 h-full flex items-end max-sm:px-6 rounded-lg transition-all"
aria-label={`Read article: ${latestPost.frontmatter.title}`}
>
<div class="gap-3 flex flex-col justify-end drop-shadow-[1px_6px_1px_rgba(0,_0,_0,_0.3)]">
<DatePub date={latestPost.frontmatter.pubDate} class="text-mint-50" />
<h2
id="post-title"
class="text-4xl max-xl:text-3xl max-sm:text-2xl font-bold"
>
<span>{latestPost.frontmatter.title}</span>
</h2>
<ReadMore class="text-mint-50" />
</div>
</a>
<div
class="gap-2 mt-3 justify-start items-center flex flex-row flex-wrap max-sm:px-6 max-sm:pb-6"
role="list"
aria-label="Article tags"
>
{tags.map((tag) => (
<Tag tag={tag} forceDark="true">{tag}</Tag>
))}
</div>
</article>
</div>
)
}

View File

@@ -0,0 +1,82 @@
---
import BlogPost from "./BlogPost.astro";
import Heading from "../ui/Heading.astro";
// Prop to determine whether to exclude the latest post or a specific post
export interface Props {
excludeLatest?: boolean;
currentPostUrl?: string;
all?: boolean;
}
const { excludeLatest = false, currentPostUrl = "", all = false } = Astro.props;
const allPosts = await Astro.glob("../../pages/blog/posts/*.md");
// Sort by date in descending order (newest first)
allPosts.sort((a, b) => {
const dateA = new Date(a.frontmatter.pubDate).getTime();
const dateB = new Date(b.frontmatter.pubDate).getTime();
return dateB - dateA;
});
// Filter posts according to props
let postsToShow = allPosts;
if (currentPostUrl) {
// Exclude current post if its URL is provided
postsToShow = postsToShow.filter((post) => {
if (!post.url) return false; // If no URL, keep the post
// Normalize URLs for comparison
const normalizedPostUrl = post.url.replace(/\/$/, ""); // Remove trailing slash if exists
const normalizedCurrentUrl = currentPostUrl.replace(/\/$/, ""); // Remove trailing slash if exists
return normalizedPostUrl !== normalizedCurrentUrl;
});
} else if (excludeLatest) {
// If no specific URL but want to exclude the latest
postsToShow = postsToShow.slice(1);
}
// Limit to 4 posts if all is false
if (!all) {
postsToShow = postsToShow.slice(0, 4);
}
---
<section
class="py-8 max-lg:px-4 max-md:px-8 max-sm:px-0 max-md:py-4 max-w-4xl mx-auto"
>
{
all && (
<div class="flex gap-4 pb-6 items-center text-center justify-center">
<Heading text="All" textGradient="Posts" level={2} />
</div>
)
}
<div class="flex flex-col gap-8 w-full mx-auto">
{
postsToShow.map((post) => (
<BlogPost
url={post.url}
title={post.frontmatter.title}
date={post.frontmatter.pubDate}
tags={post.frontmatter.tags}
languages={post.frontmatter.languages}
image={post.frontmatter.image}
/>
))
}
</div>
{
!all && (
<div id="morePosts" class="w-full flex justify-center text-center my-12">
<a
href="/blog/todas-las-publicaciones"
class="font-bold cursor-pointer text-mint-400 dark:text-mint-100 hover:text-mint-500 dark:hover:text-mint-300 transition-all"
>
View all posts...
</a>
</div>
)
}
</section>

View File

@@ -0,0 +1,32 @@
---
const allPosts = await Astro.glob("../../pages/blog/posts/*.md");
import Tag from "../ui/Tag.astro";
const tags = [...new Set(allPosts.map((post) => post.frontmatter.tags).flat())];
type VariantType = "default" | "vertical" | "compact";
const { variant = "default" } = Astro.props as { variant?: VariantType };
// Common base classes
const baseClasses = "max-w-7xl";
// Variant-specific classes
const variantClasses: Record<VariantType, string> = {
default: "max-lg:px-8 py-8 max-md:py-4 flex-wrap mx-auto gap-4 max-sm:gap-3 justify-center items-center flex flex-row",
vertical: "gap-6 justify-start items-start flex flex-col",
compact: "flex-wrap mx-auto gap-2 max-sm:gap-3 justify-start flex flex-row"
};
// Combine base classes with variant-specific classes
const classes = `${baseClasses} ${variantClasses[variant]}`;
---
<div id="tags" class={classes}>
{tags.map((tag) => <Tag tag={tag}></Tag>)}
</div>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,38 @@
---
import Heading from "../ui/Heading.astro";
import Button from "../ui/Button.astro";
const mail = "hello@efeele.dev";
const title = "Ready to take your idea to the next level?";
const subtitle = "Let's work together.";
const buttonText = "Contact Me";
const buttonIcon = "paperplane";
---
<section>
<div
class="w-full border-b bg-gradient-to-b from-mint-100 to-transparent dark:from-mint-800 dark:border-mint-950 border-mint-100"
>
<div class="mx-auto max-w-5xl px-8 py-24 max-sm:py-12">
<div class="flex items-center justify-between max-sm:flex-col max-sm:gap-8 max-sm:text-center">
<div class="max-w-lg">
<Heading
text={title}
textGradient={subtitle}
level={2}
/>
</div>
<div
class="transition-all duration-500 group-hover:animate-jump group-hover:animate-duration-[300ms] group-hover:animate-ease-in-out group-hover:scale-105 hover:scale-125"
>
<Button
variant="big"
link={`mailto:${mail}`}
text={buttonText}
iconName={buttonIcon}
/>
</div>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,39 @@
---
import ExperienceItem from "./ExperienceItem.astro";
const EXPERIENCE = [
{
date: "2021 - Present",
title: "Software Development Coordinator",
company: "EFEELE.DEV",
description:
"I lead the planning, development, and implementation of software projects to optimize administrative processes and improve digital services. I manage and provide support for technology platforms, coordinating development teams to deliver efficient solutions.",
},
{
date: "2019 - 2020",
title: "Software Development Coordinator",
company: "EFEELE.DEV",
description:
"I lead the planning, development, and implementation of software projects to optimize administrative processes and improve digital services. I manage and provide support for technology platforms, coordinating development teams to deliver efficient solutions.",
},
];
---
<div class="relative max-md:mt-0 mt-8" aria-label="Professional experience">
<ol class="relative mt-10">
{
EXPERIENCE.map((experience, index) => (
<li class="">
<article role="article" aria-labelledby={`experience-title-${index}`}>
<div
class="flex flex-col gap-2 text-zinc-00 dark:text-zinc-300 md:col-span-3"
aria-describedby={`experience-title-${index}`}
>
<ExperienceItem {...experience} />
</div>
</article>
</li>
))
}
</ol>
</div>

View File

@@ -0,0 +1,41 @@
---
interface Props {
title: string;
company: string;
description: string;
link?: string;
date: string;
}
const { title, company, description, link, date } = Astro.props;
---
<div
class="relative mx-12 pb-12 grid md:grid-cols-5 md:gap-10 before:absolute before:left-[-35px] before:block before:h-full before:border-l-2 before:border-black/20 dark:before:border-white/15 before:content-['']"
>
<div class="pb-12 md:col-span-2">
<div class="sticky top-0">
<span
class="absolute -left-[42px] text-5xl text-mint-400 rounded-full drop-shadow-[0px_0px_8px_rgba(0,_255,_94,_1)]"
>&bull;</span
>
<h3
class="text-xl font-bold text-mint-400"
>
{title}
</h3>
<h4 class="text-xl font-semibold text-zinc-600 dark:text-white">
{company}
</h4>
<time datetime={date} class="text-sm text-zinc-600/80 dark:text-white/80">
{date}
</time>
</div>
</div>
<div
class="flex flex-col gap-2 pb-4 text-zinc-00 dark:text-zinc-300 md:col-span-3"
>
{description}
</div>
</div>

View File

@@ -0,0 +1,210 @@
---
import Button from "../ui/Button.astro";
import Heading from "../ui/Heading.astro";
import Tools from "../portfolio/Tools.astro";
import { Icon } from "astro-icon/components";
import Hobbies from "../portfolio/Hobbies.astro";
const {
profileImage = "/images/efeeleprofile.webp",
profileAlt = "Photo of Fernando Aldair López Ponce (EFEELE) for the blog",
profileLink = "/about-me",
profileTitle = "FrontEnd Developer",
profileName = "Fernando López",
githubLink = "https://github.com/EFEELE/NeonMint",
githubText = "Do you like this blog's design?",
portfolioImage = "/images/portfolio.webp",
email = "hello@efeele.dev",
} = Astro.props;;
---
<section
class="scroll-m-16 px-8 max-sm:px-4 py-8"
id="home"
aria-label="Professional profile and introduction"
>
<div
class="mx-auto mb-4 h-96 w-full max-w-7xl rounded-2xl bg-gradient-to-r from-mint-300 dark:from-mint-600 to-mint-50 dark:to-mint-200/5 hover:to-mint-300/30 dark:hover:to-mint-600/30 p-[.2rem] max-md:h-auto"
>
<div
class="group relative z-0 flex h-full items-center justify-center gap-8 overflow-hidden rounded-2xl bg-gradient-to-tr from-riptide-100 to-white p-4 transition-all hover:shadow-[0_10px_50px_rgba(13,_188,_130,_0.2)] dark:bg-gradient-to-r dark:from-mint-950 dark:to-zinc-950 dark:overflow-hidden max-md:w-full max-md:gap-3 max-lg:gap-2 max-sm:flex-col dark:before:[background-image:radial-gradient(circle,_rgba(13,188,130)_0,_rgba(1,45,34)_100%)] before:[background-image:radial-gradient(circle,_rgba(95,255,202)_0,_rgba(144,253,210)_100%)] before:absolute before:left-65 before:top-10 before:h-[40%] before:aspect-square before:rounded-full before:blur-3xl before:opacity-80 before:-z-10 before:transition hover:before:animate-pulse"
>
<div class="aspect-square h-4/5 max-md:size-32">
<div
class="group-hover:scale-105 relative z-10 flex w-full cursor-pointer items-center overflow-hidden rounded-full dark:p-[1.5px] p-[2px] transition-all duration-500"
>
<div
class="absolute inset-0 h-full w-full rounded-fullfrom-transparent to-riptide-500 dark:from-transparent f dark:to-mint-300 animate-rotate-border bg-conic/[from_var(--border-angle)] "
>
</div>
<div class="relative z-20 flex w-full rounded-full bg-mint-900">
<div
class="w-full aspect-square rounded-full bg-center bg-cover"
style={`background-image: url(${profileImage})`}
aria-label="Photo of Fernando Aldair López Ponce (EFEELE) for the blog"
>
<a
href={profileLink}
class="flex h-full w-full"
aria-label="View about me page"></a>
</div>
</div>
</div>
</div>
<div
class="flex h-full w-7/12 flex-col justify-center gap-4 p-4 max-md:w-full max-md:gap-2"
>
<span
class="relative inline-flex items-center text-xs font-semibold leading-0 dark:text-white text-blacktext max-md:font-medium max-sm:justify-center"
>
<span
class="before:mr-2 before:block before:h-1.5 before:w-1.5 before:rounded-full before:bg-green-400 before:shadow-[0px_0px_0px_3px_rgba(34,_197,_94,_0.5)] before:animate-pulse before:content-['']"
></span>
Available for work
</span>
<h1
class="text-4xl font-black leading-none text-blacktext dark:text-white max-xl:text-3xl max-lg:text-2xl max-sm:text-center"
>
{profileName} 👋
</h1>
<p
class="mb-0 text-lg font-semibold leading-8 text-blacktext dark:text-gray-200 max-xl:text-base"
>
<span
class="bg-gradient-to-r from-riptide-500 to-mint-500 bg-clip-text font-black text-transparent dark:from-riptide-300 dark:to-mint-200"
>{profileTitle}</span
> with <span
class="bg-gradient-to-r from-riptide-500 to-mint-500 bg-clip-text font-black text-transparent dark:from-riptide-300 dark:to-mint-200"
>8 years of experience</span
>, passionate about development, technology, and coffee that sparks ideas. I love bringing digital projects to life. 🚀☕
</p>
<div class="flex gap-4 max-sm:flex-col max-sm:items-center">
<Button
link={"mailto:" + email}
text="Contact Me"
iconName="envelope"
/>
<Button
link={profileLink}
text="About Me"
iconName="person"
variant="dark"
/>
</div>
</div>
</div>
</div>
<div
class="mx-auto grid max-w-7xl grid-cols-2 grid-rows-4 max-sm:gap-3 gap-4 max-md:h-[80vh] max-sm:h-[900px] max-xl:h-[550px] xl:h-[700px] md:grid-cols-4 md:grid-rows-2"
>
<div
class="col-start-2 row-start-2 flex flex-col gap-3 rounded-2xl border border-emerald-50 bg-gradient-to-r from-riptide-200 to-mint-200 p-8 dark:border-zinc-800 dark:border-2 dark:bg-zinc-800 dark:bg-gradient-radial dark:from-riptide-500 dark:to-mint-500 max-md:gap-0 max-xl:p-5 max-md:p-4 max-lg:gap-1 md:col-start-4"
>
<div class="flex flex-col-reverse items-start gap-4 max-md:gap-2">
<Heading text="Beyond the Code" level={3} />
</div>
<div class="overflow-y-auto">
<Hobbies />
</div>
</div>
<div
class="col-start-1 row-start-2 rounded-2xl bg-gradient-to-r from-mint-300 dark:from-mint-600 to-mint-50 dark:to-mint-200/5 hover:to-mint-300/30 dark:hover:to-mint-600/30 p-[.2rem] transition-all ease-in duration-150 hover:scale-105 hover:shadow-[0_20px_50px_rgba(13,_188,_130,_0.4)] z-40 md:col-start-3"
>
<div class="h-full w-full overflow-hidden rounded-2xl dark:bg-zinc-900">
<div
class="relative h-full w-full overflow-hidden rounded-2xl bg-gradient-to-tr from-riptide-100 to-white dark:from-transparent dark:bg-gradient-to-bl dark:to-transparent "
>
<a
target="_blank"
href={githubLink}
aria-label="View this site's code repository on GitHub"
>
<div
class="relative flex h-full flex-col gap-8 p-8 dark:before:[background-image:radial-gradient(circle,_rgba(13,188,130)_0,_rgba(1,45,34)_100%)] before:[background-image:radial-gradient(circle,_rgba(95,255,202)_0,_rgba(144,253,210)_100%)] before:absolute before:left-30 before:bottom-30 before:h-[30%] before:aspect-square before:rounded-full before:blur-3xl before:opacity-90 before:transition before:z-0 max-xl:p-5 max-md:p-4 xl:before:bottom-10"
>
<div class="z-2">
<Heading text="Do you like this site's design?" level={3}/>
</div>
<Icon
class="absolute -bottom-10 left-30 size-50 text-mint-500/30 group-hover:animate-pulse ease-in-out max-sm:left-10 max-md:left-30 xl:size-56 xl:-bottom-5 xl:-right-24"
name="github"
aria-hidden="true"
/>
</div>
</a>
</div>
</div>
</div>
<div
class="col-span-2 col-start-1 row-start-1 rounded-2xl bg-gradient-to-r from-mint-300 dark:from-mint-600 to-mint-50 dark:to-mint-200/5 hover:to-mint-300/30 dark:hover:to-mint-600/30 p-[.2rem] md:col-start-3"
>
<article
class="group relative z-0 flex h-full w-full flex-col overflow-hidden rounded-2xl bg-gradient-to-tr from-riptide-100 to-mint-100 p-6 transition hover:shadow-[0_10px_50px_rgba(13,_188,_130,_0.2)] dark:bg-gradient-to-r dark:from-mint-900 dark:to-mint-950 dark:overflow-hidden max-md:p-4 gap-6 max-md:gap-3 max-lg:gap-4 dark:before:[background-image:radial-gradient(circle,_rgba(13,188,130)_0,_rgba(1,45,34)_100%)] before:[background-image:radial-gradient(circle,_rgba(95,255,202)_0,_rgba(144,253,210)_100%)] before:absolute before:left-30 before:-bottom-0 before:h-[80%] before:aspect-square before:rounded-full before:blur-3xl dark:before:opacity-80 before:opacity-30 before:-z-10 before:transition"
>
<div class="flex items-center gap-4 max-md:gap-2">
<Icon
class="text-3xl dark:text-white text-mint-500 max-md:text-2xl max-sm:text-sm"
name="dashboard"
aria-hidden="true"
/>
<Heading text="Tech" textGradient="Stack" level={3} />
</div>
<div class="overflow-y-auto">
<Tools />
</div>
</article>
</div>
<div
class="col-span-2 row-span-2 rounded-2xl bg-gradient-to-r from-mint-300 dark:from-mint-600 to-mint-50 dark:to-mint-200/5 p-[.2rem] hover:to-mint-300/30 dark:hover:to-mint-600/30 md:col-start-1 md:row-start-1"
>
<article
class="group relative flex h-full flex-col justify-start overflow-hidden rounded-2xl bg-gradient-to-br from-riptide-100 to-white dark:from-mint-950 dark:to-zinc-950 p-8 transition-all hover:shadow-[0_20px_50px_rgba(13,_188,_130,_0.4)] max-md:p-6 dark:before:[background-image:radial-gradient(circle,_rgba(13,188,130)_0,_rgba(1,45,34)_100%)] before:[background-image:radial-gradient(circle,_rgba(95,255,202)_0,_rgba(144,253,210)_100%)] before:absolute before:left-1/2 before:bottom-30 before:h-[30%] before:aspect-square before:rounded-full before:blur-3xl before:opacity-70 before:transition before:-z-0 hover:before:animate-pulse xl:before:bottom-2/5"
>
<a
href="#projects"
class="absolute left-0 top-0 z-50 h-full w-full bg-transparent"
aria-label="View projects section"></a>
<div class="flex items-center gap-4 max-md:gap-2">
<Icon
class="text-3xl dark:text-white text-mint-500 max-md:text-2xl max-sm:text-xl"
name="code"
aria-hidden="true"
/>
<Heading text="Projects" level={3}/>
</div>
<p
class="z-2 my-6 text-lg font-semibold leading-6 dark:text-gray-200 text-blacktext max-xl:text-base max-w-[90%] max-md:max-w-[85%]"
>
I love <span
class="bg-gradient-to-r from-riptide-500 to-mint-500 bg-clip-text font-black text-transparent dark:from-riptide-300 dark:to-mint-200"
>turning ideas into real projects.</span
>
<br /> Here I show you some of the <span
class="bg-gradient-to-r from-riptide-500 to-mint-500 bg-clip-text font-black text-transparent dark:from-riptide-300 dark:to-mint-200"
>developments</span
> I've worked on, applying technology, design, and lots of creativity.
<span
class="bg-gradient-to-r from-riptide-500 to-mint-500 font-black bg-clip-text text-transparent dark:from-riptide-300 dark:to-mint-200"
>Check them out!</span
>
</p>
<img
class="relative left-0 z-0 mt-40 w-full object-cover transition-all ease-in-out duration-500 group-hover:rotate-2 group-hover:scale-185 max-xl:mt-28 max-lg:mt-10 max-sm:mt-0 max-md:scale-100 max-sm:scale-125 scale-180 max-md:group-hover:scale-105 before:[background-image:radial-gradient(circle,_rgba(13,188,130)_0,_rgba(1,45,34)_100%)] before:absolute before:left-30 before:-bottom-0 before:h-[80%] before:aspect-square before:rounded-full before:blur-3xl before:opacity-80 before:-z-10 before:transition"
src={portfolioImage}
alt="View of two website interfaces with modern and dark design. The first screen shows the title 'Behind the Code' with a rocket and dark background, explaining the origin of a community software project. The second screen presents a platform for finding speakers, with an attractive design, expert photos, and a prominent search button."
loading="lazy"
width="480"
height="320"
/>
</article>
</div>
</div>
</section>

View File

@@ -0,0 +1,9 @@
---
const { tag } = Astro.props;
---
<span
class="px-4 py-1 rounded-full text-sm font-medium text-zinc-500 dark:text-neutral-400 hover:text-blacktext bg-riptide-50 dark:bg-zinc-800 hover:bg-mint-200 cursor-default transition-all ease-in-out duration-300"
>
{tag}
</span>

View File

@@ -0,0 +1,37 @@
---
interface Props {
variant?: 'default' | 'vertical';
}
const hobbies = [
"Writing",
"Camping",
"Coffee",
"Music",
"Movies",
"Board Games"
];
import Hobbie from "./Hobbie.astro";
const baseClasses = "max-w-7xl gap-3 py-4 max-md:py-3";
const variants = {
default: `${baseClasses} flex-wrap mx-auto max-md:gap-2 max-sm:gap-1 justify-start items-center flex flex-row`,
vertical: `${baseClasses} max-sm:gap-2 justify-start items-start flex flex-col`
} as const;
const { variant = "default" } = Astro.props as Props;
const classes = variants[variant];
---
<div id="tags" class={classes}>
{hobbies.map((hobby) => <Hobbie tag={hobby} />)}
</div>

View File

@@ -0,0 +1,61 @@
---
import Project from "./Project.astro";
const allPosts = await Astro.glob("../../pages/portfolio/projects/*.md");
// Sort by descending date (most recent first)
allPosts.sort(
(a, b) =>
new Date(b.frontmatter.pubDate).getTime() -
new Date(a.frontmatter.pubDate).getTime(),
);
---
<section class="relative pt-8 pb-32 max-2xl:px-8 max-md:pt-4">
<div class="mx-auto max-w-7xl py-8">
<slot />
</div>
<div
id="containerProjects"
class="mx-auto max-w-7xl grid grid-cols-3 max-lg:grid-cols-2 max-md:grid-cols-1 gap-5 p-2 py-4 max-h-[150vh] overflow-hidden transition-[max-height] duration-500 ease-in-out"
>
{
allPosts.map((post) => (
<Project
url={post.url}
title={post.frontmatter.title}
description={post.frontmatter.description}
date={post.frontmatter.pubDate}
languages={post.frontmatter.languages}
image={post.frontmatter.image}
/>
))
}
</div>
<div
id="moreProjects"
class="absolute bottom-0 left-0 w-full flex justify-center text-center bg-gradient-to-t from-[#FBFEFD] dark:from-[#0e0e10] from-55% to-transparent to-100% pb-30 pt-52"
>
<button
class="absolute font-bold cursor-pointer text-mint-400 dark:text-mint-100 hover:text-mint-500 dark:hover:text-mint-300 transition-all"
>
View More Projects...
</button>
</div>
</section>
<script>
document.querySelector("#moreProjects")?.addEventListener("click", () => {
const contenedor = document.querySelector("#containerProjects") as HTMLElement;
const moreprojects = document.querySelector("#moreProjects");
if (contenedor && moreprojects) {
// Use a very large max height to show all content
contenedor.style.maxHeight = "15000px";
// Hide the view more button
setTimeout(() => {
moreprojects.classList.add("hidden");
}, 5000);
}
});
</script>

View File

@@ -0,0 +1,32 @@
---
const { title, url, image, languages, description, size = "xs" } = Astro.props;
import Capsule from "../ui/Capsule.astro";
---
<a href={url} class="w-full h-full hover:scale-105 transition-all duration-300 ease-in-out rounded-3xl bg-gradient-to-br from-mint-50/50 to-mint-300/10 dark:from-zinc-800 dark:to-mint-800/10">
<article class="group h-full gap-4 p-4 max-md:p-6 flex flex-col justify-start items-center cursor-pointer transition-all duration-300 ease-in-out rounded-3xl bg-transparent backdrop-blur-lg overflow-hidden hover:shadow-[0_10px_10px_rgba(0,_0,_0,_0.05)] dark:border-0 dark:hover:shadow-[0_10px_10px_rgba(13,_188,_130,_0.05)] before:absolute before:size-36 before:aspect-square before:rounded-full before:blur-3xl dark:before:blur-3xl dark:before:opacity-20 before:opacity-5 before:transition before:-z-1 before:left-7/12 before:bottom-50 xl:before:-bottom-0 before:[background-image:radial-gradient(circle,_rgba(71,255,194)_0,_rgba(0,255,191)_100%)] dark:before:[background-image:radial-gradient(circle,_rgba(7,255,173)_0,_rgba(1,45,34)_100%)] after:absolute after:size-40 after:aspect-square after:rounded-full dark:after:blur-2xl after:blur-3xl dark:after:opacity-20 after:opacity-10 after:transition after:-z-1 after:-left-10 after:top-1/2 after:[background-image:radial-gradient(circle,_rgba(0,255,217)_0,_rgba(121,249,255,65%)_70%)] dark:after:[background-image:radial-gradient(circle,_rgba(0,255,218)_0,_rgba(2,181,190,65%)_70%)]">
<div class="overflow-hidden rounded-xl">
<img class="h-auto rounded-xl transition-all duration-300 ease-in-out group-hover:scale-105" src={image.url} alt={image.alt} />
</div>
<div class="flex flex-col p-2">
<div class="flex flex-col gap-3 w-full">
<h2 class="text-3xl font-bold text-blacktext dark:text-mint-50">
{title}
</h2>
<div class="flex flex-wrap gap-3">
{
languages.map((language: string) => (
<Capsule size={size} linkEnabled={false} lang={language} />
))
}
</div>
<p class="text-lg max-xl:text-base w-full max-md:max-w-[85%] my-2 leading-6 font-medium text-blacktext dark:text-gray-200">
{description}
</p>
</div>
</div>
</article>
</a>

View File

@@ -0,0 +1,54 @@
---
const allTools = [
"html",
"javascript",
"ts",
"angular",
"astro",
"node",
"css",
"tailwind",
"sass",
"bootstrap",
"mongo",
"firebase",
"mysql",
"cloudflare",
"figma",
"vercel",
"netlify",
"git",
"markdown",
"php",
"wordpress",
];
import Capsule from "../ui/Capsule.astro";
type ToolVariant = "default" | "center";
// Base classes that are common to all variants
const baseClasses = "flex flex-wrap gap-4 max-lg:gap-1 grid-auto-efe";
// Variant-specific classes
const variantClasses: Record<ToolVariant, string> = {
default: "cursor-default",
center: "justify-center cursor-default",
};
const { variant = "default", linkEnabled = false, size = "md" } = Astro.props;
// Combine base classes with variant-specific classes
const classes = `${baseClasses} ${variantClasses[variant as ToolVariant]}`;
---
<div class={classes}>
{allTools.map((tool) => <Capsule lang={tool} linkEnabled={linkEnabled} size={size} />)}
</div>

View File

@@ -0,0 +1,166 @@
---
type ButtonVariant = "default" | "big" | "dark";
interface Props {
link: string;
text: string;
iconName?: string;
variant?: ButtonVariant;
ariaLabel?: string;
disabled?: boolean;
isLoading?: boolean;
isExternal?: boolean;
isActive?: boolean;
class?: string;
}
const {
link,
text,
iconName,
variant = "default",
ariaLabel,
disabled = false,
isLoading = false,
isExternal = false,
isActive = false,
class: className = ""
} = Astro.props as Props;
import { Icon } from "astro-icon/components";
// Common base classes for all buttons
const baseClasses = `
z-2
text-center
cursor-pointer
leading-none
hover:scale-110
w-fit
font-medium
flex
gap-2
transition-all
ease-in-out
justify-center
items-center
rounded-full
disabled:opacity-50
disabled:cursor-not-allowed
disabled:hover:scale-100
disabled:hover:bg-none
disabled:hover:shadow-none
aria-disabled:opacity-50
aria-disabled:cursor-not-allowed
aria-disabled:hover:scale-100
aria-disabled:hover:bg-none
aria-disabled:hover:shadow-none
`;
// Specific classes for each variant
const variantClasses = {
default: `
text-blacktext/90
dark:text-mint-50
dark:hover:text-white
px-6
py-4
max-xl:px-5
max-sm:py-2
max-sm:px-3
text-lg
max-xl:text-base
max-sm:text-sm
hover:bg-gradient-to-l
bg-gradient-to-r
from-riptide-300
to-mint-300
dark:from-riptide-500
dark:to-mint-500
`,
big: `
font-normal
gap-3
max-md:gap-1
text-blacktext
dark:text-mint-50
dark:hover:text-white
px-8
max-sm:py-3
py-5
max-xl:px-6
max-sm:px-3
text-2xl
max-xl:text-xl
max-sm:text-lg
hover:bg-gradient-to-l
bg-gradient-to-r
from-riptide-200
to-mint-200
dark:from-riptide-500
dark:to-mint-500
`,
dark: `
group
dark:text-mint-50
text-blacktext
dark:hover:text-white
px-6
py-4
max-xl:px-5
max-sm:py-2
max-sm:px-3
text-lg
max-xl:text-base
max-sm:text-sm
dark:bg-zinc-800
bg-white
dark:hover:bg-mint-500
`,
};
// Icon classes for each variant
const iconClasses = {
default: "size-5",
big: "size-5",
dark: "size-5 dark:text-mint-300 text-blacktext group-hover:text-blacktext dark:group-hover:text-white transition-colors",
};
// Combine base classes with variant-specific classes
const buttonClasses = `${baseClasses} ${variantClasses[variant]}`;
// Generate aria-label if not provided
const buttonAriaLabel = ariaLabel || (iconName ? `${text} ${iconName}` : text);
// Determine if it's an external link
const isExternalLink = isExternal || link.startsWith('http');
// Additional attributes for external links
const externalAttributes = isExternalLink ? {
target: '_blank',
rel: 'noopener noreferrer'
} : {};
// Loading state
const loadingText = isLoading ? 'Loading...' : text;
const loadingAriaLabel = isLoading ? `${buttonAriaLabel} - Loading` : buttonAriaLabel;
---
<div
class="w-fit h-fit from-transparent via-riptide-300 to-transparent dark:from-transparent dark:via-mint-500 dark:to-transparent from-40% to-60% animate-rotate-border bg-conic/[from_var(--border-angle)] p-px hover:shadow-lg
hover:shadow-mint-500/30 rounded-full"
>
<a
class:list={[buttonClasses, className]}
href={disabled ? '#' : link}
aria-label={loadingAriaLabel}
role="button"
aria-disabled={disabled}
aria-busy={isLoading}
aria-pressed={isActive}
{...externalAttributes}
>
{iconName && <Icon name={iconName} class={iconClasses[variant]} aria-hidden="true" />}
{isLoading && <Icon name="mdi:loading" class="animate-spin" aria-hidden="true" />}
{loadingText}
</a>
</div>

View File

@@ -0,0 +1,87 @@
---
import { Icon } from "astro-icon/components";
import { getLanguage } from "../../utils/languages";
interface Props {
lang: string;
size?: "xs" | "md";
linkEnabled?: boolean;
}
const { size = "md", lang, linkEnabled = true } = Astro.props as Props;
const baseClasses = {
container: [
"flex items-center w-fit",
"pl-2 pr-2 py-0.5 gap-1",
"text-sm font-semibold leading-3",
"bg-white shadow rounded-full",
"transition-all duration-300 ease-in-out hover:bg-zinc-800 hover:text-white",
"max-sm:pl-1 max-sm:pr-1.5 max-sm:text-xs max-sm:gap-0.5",
].join(" "),
iconContainer: [
"flex items-center justify-center",
"aspect-square",
"bg-black rounded-full p-1",
].join(" "),
};
const sizeClasses = {
xs: "size-5",
md: "size-7",
};
const selectedLanguage = getLanguage(lang);
const getContainerClasses = () => {
const textSize = selectedLanguage.name.length > 10 ? "text-sm" : "text-base";
return `${baseClasses.container} ${textSize}`;
};
const getIconContainerClasses = () => {
return `${baseClasses.iconContainer} ${sizeClasses[size]} max-lg:size-6 max-sm:size-5 ${
selectedLanguage.className ? selectedLanguage.className : ""
}`;
};
---
{
linkEnabled ? (
<a
class="cursor-pointer"
href={`/blog/techs/${lang}`}
aria-label={`View articles about ${selectedLanguage.name}`}
role="link"
>
<span
class={getContainerClasses()}
role="presentation"
aria-hidden="true"
>
<div
class={getIconContainerClasses()}
role="img"
aria-label={`${selectedLanguage.name} icon`}
>
<Icon class="!w-full" name={selectedLanguage.iconName} />
</div>
{selectedLanguage.name}
</span>
</a>
) : (
<span
class={`${getContainerClasses()} cursor-default`}
role="presentation"
aria-hidden="true"
>
<div
class={getIconContainerClasses()}
role="img"
aria-label={`${selectedLanguage.name} icon`}
>
<Icon class="!w-full" name={selectedLanguage.iconName} />
</div>
{selectedLanguage.name}
</span>
)
}

View File

@@ -0,0 +1,35 @@
---
interface Props {
text?: string;
textGradient?: string;
level?: 1 | 2 | 3;
className?: string;
}
const { text = "", textGradient = "", level = 1, className = "" } = Astro.props;
const Tag = `h${level}`;
---
<Tag class:list={[
"font-extrabold text-4xl max-xl:text-3xl",
level === 2 && "max-md:text-3xl ",
level === 3 && "max-md:text-xl ",
"dark:text-white text-blacktext",
className
]}>
{text && (
<>
{text}
{textGradient && " "}
</>
)}
{textGradient && (
<b
class:list={[
"bg-gradient-to-r from-riptide-500 to-mint-400 dark:from-riptide-500 dark:to-mint-500 text-transparent bg-clip-text",
level === 1
]}
>{textGradient}</b>
)}
</Tag>

View File

@@ -0,0 +1,10 @@
---
const {class: className} = Astro.props;
import { Icon } from "astro-icon/components";
---
<div
class={`flex justify-start items-center gap-2 text-blacktext dark:text-mint-50 ${className || ''}`}
>
<span class="font-medium tracking-wider">Read more </span>
<Icon name="arrow-left" class="size-4 rotate-180" />
</div>

View File

@@ -0,0 +1,41 @@
---
import { Icon } from "astro-icon/components";
const { tweetText, currentUrl } = Astro.props;
const shareLinks = [
{
name: "Twitter",
icon: "twitter",
href: `https://twitter.com/intent/tweet?text=${tweetText}&url=${currentUrl}`,
},
{
name: "Facebook",
icon: "facebook",
href: `https://www.facebook.com/sharer/sharer.php?u=${currentUrl}`,
},
{
name: "WhatsApp",
icon: "whatsapp",
href: `https://api.whatsapp.com/send?text=${encodeURIComponent(`${currentUrl}`)}`,
},
];
---
<div class="flex items-center gap-5 text-lg" role="group" aria-label="Share on social media">
<span class="text-base font-bold text-blacktext dark:text-white">Share </span>
{
shareLinks.map((link) => (
<a
href={link.href}
class="bg-blacktext rounded-full p-2"
target="_blank"
rel="noopener noreferrer"
aria-label={`Share on ${link.name}`}
title={`Share on ${link.name}`}
>
<Icon name={link.icon} />
</a>
))
}
</div>

View File

@@ -0,0 +1,26 @@
---
import { Icon } from "astro-icon/components"
interface Props {
iconName: string;
link: string;
label?: string;
}
const { iconName, link, label = `Link to ${iconName}` } = Astro.props;
// Validate required props
if (!iconName || !link) {
throw new Error('Social component requires both iconName and link props');
}
---
<a
class="hover:text-mint-300 hover:scale-150 transition-all"
target="_blank"
href={link}
rel="noopener noreferrer"
aria-label={label}
>
<Icon name={iconName} aria-hidden="true" />
</a>

View File

@@ -0,0 +1,10 @@
---
const { tag, forceDark = false } = Astro.props;
---
<a
href={`/blog/tags/${tag}`}
class={`max-md:text-xs text-sm font-medium ${forceDark ? 'text-neutral-400 bg-zinc-800' : 'text-zinc-500 dark:text-neutral-400 hover:text-blacktext'} transition-all ease-in-out duration-300 px-4 py-1 max-md:px-3 rounded-full ${forceDark ? 'bg-zinc-800' : 'bg-mint-950/5 dark:bg-zinc-800 hover:bg-mint-200'}`}
>
{tag}
</a>

View File

@@ -0,0 +1,75 @@
---
import { Icon } from "astro-icon/components";
---
<button id="themeToggle" class="hover:cursor-pointer hover:text-mint-400 transition-all">
<Icon name="sun" class="sun"/>
<Icon name="moon" class="moon"/>
</button>
<style>
#themeToggle {
border: 0;
background: none;
}
.sun {
display: block;
}
.moon {
display: none;
}
:global(.dark) .sun {
display: none;
}
:global(.dark) .moon {
display: block;
}
</style>
<script is:inline>
// Execute immediately before the page loads
(function() {
const theme = (() => {
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
return localStorage.getItem("theme");
}
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "light";
})();
if (theme === "light") {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
window.localStorage.setItem("theme", theme);
})();
// Listen for changes in system preferences
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
if (!localStorage.getItem("theme")) {
if (e.matches) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
});
const handleToggleClick = () => {
const element = document.documentElement;
element.classList.toggle("dark");
const isDark = element.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
};
document
.getElementById("themeToggle")
.addEventListener("click", handleToggleClick);
</script>