refactor: 清理无用资源并更新项目配置

删除大量未使用的图标、图片和组件文件
更新.gitignore、tsconfig.json和astro配置
添加新的工具函数和UI组件
修改项目元数据和依赖项
This commit is contained in:
joyzhao
2025-06-13 12:03:15 +08:00
parent 43d830aa27
commit c1bfb0915e
145 changed files with 1901 additions and 13996 deletions

View File

@@ -0,0 +1,65 @@
import React from "react";
import { awards } from "@/lib/data";
import { Trophy } from "lucide-react";
import MotionWrapper from "./MotionWrapper";
import { GlassCard } from "./ui/glass-card";
import { motion } from "framer-motion";
export default function AwardsSection() {
return (
<section
id="awards"
className="py-12 bg-gradient-to-b from-background to-muted/10"
>
<div className="container max-w-4xl mx-auto px-6 md:px-4">
<MotionWrapper>
<h2 className="text-2xl font-bold mb-8 text-center md:text-left">
🏆 Awards
</h2>
</MotionWrapper>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{awards.map((award, index) => (
<MotionWrapper key={award.name + award.date} delay={index * 0.1}>
<GlassCard className="p-4 dark:border-purple-500/10 hover:border-purple-500/30 transition-all duration-300 flex flex-col h-full">
<div className="flex items-center mb-2">
<motion.div
whileHover={{ rotate: 20 }}
transition={{ type: "spring", stiffness: 500 }}
className="flex items-center justify-center bg-gradient-to-r from-amber-500 to-yellow-500 rounded-full p-1.5 mr-2"
>
<Trophy className="h-4 w-4 text-white" />
</motion.div>
<h3 className="font-medium">{award.name}</h3>
</div>
<p className="text-xs text-muted-foreground mb-1 pl-8">
🏢 {award.issuer}
</p>
<div className="flex flex-col space-y-2 mt-auto">
<div className="flex justify-between items-center">
<span className="text-xs text-muted-foreground bg-background/50 px-2 py-1 rounded-md">
📅 {award.date}
</span>
<motion.span
className="text-xs px-2 py-1 bg-purple-500/10 rounded-full"
whileHover={{ scale: 1.05 }}
>
{award.position}
</motion.span>
</div>
<motion.span
className="text-xs text-muted-foreground/80 bg-background/50 px-2 py-1 rounded-md w-fit"
whileHover={{ scale: 1.05 }}
>
{award.type === "International" ? "🌎 " : "🇮🇳 "}
{award.type}
</motion.span>
</div>
</GlassCard>
</MotionWrapper>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,72 @@
import { education } from "@/lib/data";
import TimelineItem from "./TimelineItem";
import { Award } from "lucide-react";
import MotionWrapper from "./MotionWrapper";
import { motion } from "framer-motion";
export default function EducationSection() {
return (
<section
id="education"
className="py-12 bg-gradient-to-b from-muted/10 to-background"
>
<div className="container max-w-4xl mx-auto px-6 md:px-4">
<MotionWrapper>
<h2 className="text-2xl font-bold mb-8 text-center md:text-left">
🎓 Education
</h2>
</MotionWrapper>
<div className="mb-8">
{education.map((edu, index) => (
<TimelineItem
key={edu.institution}
title={`🎓 ${edu.degree}`}
subtitle={`🏛️ ${edu.institution}`}
date={`📅 ${edu.period}`}
isLast={index === education.length - 1}
index={index}
>
<p className="text-sm text-muted-foreground mb-3">
📍 {edu.location}
</p>
{edu.achievements && edu.achievements.length > 0 && (
<motion.div
className="mt-3 p-4 bg-background/80 backdrop-blur-sm backdrop-filter rounded-lg border border-purple-500/20 dark:bg-card/10 dark:border-purple-500/10 shadow-sm"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
>
<div className="flex items-center mb-3">
<div className="h-6 w-6 flex items-center justify-center rounded-full bg-purple-500/10 mr-2">
<Award className="h-4 w-4 text-purple-500" />
</div>
<h4 className="text-sm font-medium">
Achievements & Activities
</h4>
</div>
<ul className="list-none ml-4 space-y-2 text-sm">
{edu.achievements.map((achievement, i) => (
<motion.li
key={i}
className="text-muted-foreground relative pl-6"
initial={{ opacity: 0, x: -10 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: 0.1 * i }}
viewport={{ once: true }}
>
{achievement}
</motion.li>
))}
</ul>
</motion.div>
)}
</TimelineItem>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,72 @@
import { workExperience } from "@/lib/data";
import TimelineItem from "./TimelineItem";
import { Briefcase } from "lucide-react";
import { motion } from "framer-motion";
import MotionWrapper from "./MotionWrapper";
export default function ExperienceSection() {
return (
<section
id="experience"
className="py-12 bg-gradient-to-b from-muted/20 to-background"
>
<div className="container max-w-4xl mx-auto px-6 md:px-4">
<MotionWrapper>
<h2 className="text-2xl font-bold mb-8 text-center md:text-left flex items-center md:inline-block">
<motion.span
className="inline-block mr-2"
initial={{ rotate: 0 }}
whileInView={{ rotate: [0, -10, 10, -5, 5, 0] }}
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
>
💼
</motion.span>{" "}
Work Experience
</h2>
</MotionWrapper>
<div className="mb-8">
{workExperience.map((job, index) => (
<TimelineItem
key={job.company + job.period}
title={`👨‍💻 ${job.position} | ${job.company}`}
subtitle={`🌍 ${job.location}`}
date={`📅 ${job.period}`}
isLast={index === workExperience.length - 1}
index={index}
>
<motion.div
className="mt-3 p-4 bg-background/80 backdrop-blur-sm backdrop-filter rounded-lg border border-purple-500/20 dark:bg-card/10 dark:border-purple-500/10 shadow-sm"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
>
<div className="flex items-center mb-3">
<div className="h-6 w-6 flex items-center justify-center rounded-full bg-purple-500/10 mr-2">
<Briefcase className="h-4 w-4 text-purple-500" />
</div>
<h4 className="text-sm font-medium">Key Achievements</h4>
</div>
<ul className="list-none ml-4 space-y-2 text-sm">
{job.achievements.map((achievement, i) => (
<motion.li
key={i}
className="text-muted-foreground relative pl-6"
initial={{ opacity: 0, x: -10 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: 0.1 * i }}
viewport={{ once: true }}
>
{achievement}
</motion.li>
))}
</ul>
</motion.div>
</TimelineItem>
))}
</div>
</div>
</section>
);
}

58
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,58 @@
import { personalInfo } from "@/lib/data";
import { motion } from "framer-motion";
export default function Footer() {
return (
<footer className="border-t border-purple-500/10 py-6 bg-gradient-to-b from-background to-muted/20 backdrop-blur-sm">
<div className="container max-w-4xl mx-auto px-6 md:px-4">
<motion.div
className="flex flex-col md:flex-row justify-between items-center"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
>
<motion.p
className="text-sm text-muted-foreground text-center md:text-left"
whileHover={{ scale: 1.01 }}
>
&copy; {new Date().getFullYear()} {personalInfo.name}. All rights
reserved.
</motion.p>
<motion.p
className="text-sm text-muted-foreground mt-2 md:mt-0 text-center md:text-left"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.5 }}
viewport={{ once: true }}
whileHover={{ scale: 1.01 }}
>
Built with{" "}
<motion.span
className="inline-block"
initial={{ rotate: 0 }}
whileHover={{ rotate: 360 }}
transition={{ duration: 0.5 }}
>
💻
</motion.span>{" "}
and{" "}
<motion.span
className="inline-block"
animate={{
scale: [1, 1.2, 1],
}}
transition={{
repeat: Infinity,
repeatType: "reverse",
duration: 1.5,
}}
>
</motion.span>
</motion.p>
</motion.div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,100 @@
import ThemeToggle from "./ui/theme-toggle";
import { personalInfo } from "@/lib/data";
import { useState } from "react";
import { Menu, X } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
export default function GlassHeader() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const toggleMenu = () => setIsMenuOpen(!isMenuOpen);
return (
<header className="sticky z-50 w-full backdrop-blur-md backdrop-filter bg-background/70 dark:bg-background/40 border-b border-border/40 supports-[backdrop-filter]:bg-background/60">
<div className="container max-w-4xl mx-auto p-4 flex justify-between items-center">
<motion.a
className="flex items-center text-lg font-medium"
href="/"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{personalInfo.name}
</motion.a>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center space-x-6 text-sm font-medium">
{["experience", "skills", "projects", "awards", "education"].map(
(item, index) => (
<motion.a
key={item}
href={`#${item}`}
className="transition-colors hover:text-foreground/80 text-foreground/60"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: index * 0.1 }}
whileHover={{ y: -2 }}
>
{item === "experience" && "💼 "}
{item === "skills" && "🛠️ "}
{item === "projects" && "🚀 "}
{item === "awards" && "🏆 "}
{item === "education" && "🎓 "}
{item.charAt(0).toUpperCase() + item.slice(1)}
</motion.a>
)
)}
</nav>
<div className="flex items-center space-x-2">
<ThemeToggle />
{/* Mobile Menu Button */}
<motion.button
className="md:hidden p-2 text-foreground"
onClick={toggleMenu}
aria-label="Toggle menu"
whileTap={{ scale: 0.95 }}
>
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
</motion.button>
</div>
</div>
{/* Mobile Navigation */}
<AnimatePresence>
{isMenuOpen && (
<motion.div
className="md:hidden py-4 px-4 border-t border-border/10 backdrop-blur-md backdrop-filter bg-background/80 dark:bg-background/40"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<nav className="flex flex-col space-y-4 text-sm font-medium">
{["experience", "skills", "projects", "awards", "education"].map(
(item, index) => (
<motion.a
key={item}
href={`#${item}`}
className="transition-colors hover:text-foreground/80 text-foreground/60 py-2"
onClick={toggleMenu}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2, delay: index * 0.1 }}
>
{item === "experience" && "💼 "}
{item === "skills" && "🛠️ "}
{item === "projects" && "🚀 "}
{item === "awards" && "🏆 "}
{item === "education" && "🎓 "}
{item.charAt(0).toUpperCase() + item.slice(1)}
</motion.a>
)
)}
</nav>
</motion.div>
)}
</AnimatePresence>
</header>
);
}

View File

@@ -0,0 +1,140 @@
import { personalInfo } from "@/lib/data";
import { Mail, Github, MapPin, Linkedin } from "lucide-react";
import { motion } from "framer-motion";
import MotionWrapper from "./MotionWrapper";
export default function HeroSection() {
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.3,
},
},
};
const childVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
},
},
};
return (
<section className="py-16 md:py-24 relative overflow-hidden">
<div className="container max-w-4xl mx-auto px-6 md:px-4 relative z-10">
<motion.div
className="flex flex-col md:flex-row md:items-center justify-between mb-8"
variants={containerVariants}
initial="hidden"
animate="visible"
>
<div className="text-center md:text-left">
<motion.h1
className="text-4xl font-bold mb-2"
variants={childVariants}
>
{personalInfo.name}{" "}
<span className="inline-block animate-pulse"></span>
</motion.h1>
<motion.p
className="text-xl text-muted-foreground mb-6"
variants={childVariants}
>
Software Engineer 👨💻
</motion.p>
<motion.div
className="flex flex-col gap-2 items-center md:items-start"
variants={containerVariants}
>
<motion.div
className="flex items-center text-sm text-muted-foreground"
variants={childVariants}
whileHover={{ scale: 1.05, color: "#4b5563" }}
>
<MapPin className="h-4 w-4 mr-2" />
📍 {personalInfo.location}
</motion.div>
<motion.a
href={`mailto:${personalInfo.email}`}
className="flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
variants={childVariants}
whileHover={{ scale: 1.05, color: "#4b5563" }}
>
<Mail className="h-4 w-4 mr-2" />
{personalInfo.email}
</motion.a>
<motion.a
href={personalInfo.github}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
variants={childVariants}
whileHover={{ scale: 1.05, color: "#4b5563" }}
>
<Github className="h-4 w-4 mr-2" />
🌟 GitHub
</motion.a>
<motion.a
href={personalInfo.linkedin}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
variants={childVariants}
whileHover={{ scale: 1.05, color: "#4b5563" }}
>
<Linkedin className="h-4 w-4 mr-2" />
🔗 LinkedIn
</motion.a>
</motion.div>
</div>
<motion.div
className="mt-6 md:mt-0 flex justify-center"
variants={childVariants}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<div className="relative">
<div className="absolute -inset-1 bg-gradient-to-r from-pink-500 to-purple-500 rounded-full blur opacity-30 group-hover:opacity-100 transition duration-1000 group-hover:duration-200"></div>
<img
src="/profile.jpg"
alt="Profile"
className="w-48 md:w-60 rounded-full relative ring-2 ring-purple-500/50"
style={{ objectFit: "cover" }}
/>
</div>
</motion.div>
</motion.div>
<MotionWrapper>
<div className="bg-gradient-to-r from-purple-500/10 to-pink-500/10 backdrop-blur-sm backdrop-filter p-4 rounded-lg border border-purple-500/20 dark:border-purple-500/10 shadow-sm">
<p className="text-muted-foreground pl-4 py-2 mb-4 relative">
<span className="absolute left-0 top-0 h-full w-1 bg-gradient-to-b from-purple-500 to-pink-500 rounded-full"></span>
🚀 Passionate software engineer with a versatile skill set
spanning multiple domains. I thrive on solving complex challenges
across different platforms and environments, adapting quickly to
new technologies and methodologies. My holistic approach combines
technical expertise with creative problem-solving, allowing me to
develop solutions that are both innovative and practical. I'm
driven by continuous learning and a commitment to excellence,
whether working independently or collaborating with diverse teams
to create impactful, scalable solutions.
</p>
</div>
</MotionWrapper>
</div>
</section>
);
}

View File

@@ -0,0 +1,41 @@
import React from "react";
import { motion } from "framer-motion";
import type { MotionProps } from "framer-motion";
interface MotionWrapperProps extends MotionProps {
children: React.ReactNode;
delay?: number;
}
// Default animations for sections
const defaultAnimations = {
hidden: { opacity: 0, y: 20 },
visible: (delay: number = 0) => ({
opacity: 1,
y: 0,
transition: {
duration: 0.6,
delay: delay,
ease: "easeOut",
},
}),
};
export default function MotionWrapper({
children,
delay = 0,
...props
}: MotionWrapperProps) {
return (
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={defaultAnimations}
custom={delay}
{...props}
>
{children}
</motion.div>
);
}

View File

@@ -0,0 +1,70 @@
import React from "react";
import { projects } from "@/lib/data";
import {
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "./ui/card";
import { Github } from "lucide-react";
import { GlassCard } from "./ui/glass-card";
import MotionWrapper from "./MotionWrapper";
import { motion } from "framer-motion";
export default function ProjectsSection() {
return (
<section id="projects" className="py-12 relative">
<div className="container max-w-4xl mx-auto px-6 md:px-4">
<MotionWrapper>
<h2 className="text-2xl font-bold mb-8 text-center md:text-left">
🚀 Projects
</h2>
</MotionWrapper>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{projects.map((project, index) => (
<MotionWrapper key={project.title} delay={index * 0.2}>
<GlassCard className="group overflow-hidden dark:border-purple-500/10 h-full flex flex-col">
<CardHeader className="bg-gradient-to-r from-purple-500/5 to-pink-500/5">
<CardTitle className="text-center md:text-left group-hover:text-purple-500 transition-colors duration-300">
{project.title}
</CardTitle>
</CardHeader>
<CardContent className="flex-grow">
<ul className="list-disc ml-4 space-y-1 text-sm group-hover:space-y-2 transition-all duration-300">
{project.description.map((desc, i) => (
<motion.li
key={i}
className="text-muted-foreground"
initial={{ opacity: 0, x: -10 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.1 }}
viewport={{ once: true }}
>
{desc}
</motion.li>
))}
</ul>
</CardContent>
<CardFooter className="flex justify-center md:justify-start items-center border-t border-border/30 bg-gradient-to-r from-purple-500/5 to-pink-500/5">
<motion.a
href={project.github}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-sm text-muted-foreground hover:text-purple-500 transition-colors group/link pt-8"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Github className="h-4 w-4 mr-2 group-hover/link:rotate-12 transition-transform duration-300" />
View on GitHub 🔗
</motion.a>
</CardFooter>
</GlassCard>
</MotionWrapper>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,148 @@
import React from "react";
import { skills } from "@/lib/data";
import { motion } from "framer-motion";
import MotionWrapper from "./MotionWrapper";
import { GlassCard } from "./ui/glass-card";
function SkillTag({ skill, index }: { skill: string; index: number }) {
return (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{
type: "spring",
stiffness: 260,
damping: 20,
delay: 0.05 * index,
}}
whileHover={{ scale: 1.05, y: -2 }}
className="px-3 py-1 bg-muted/80 backdrop-blur-sm rounded-md text-sm border border-purple-500/10 shadow-sm"
>
{skill}
</motion.div>
);
}
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const skillCategoryVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
},
},
};
export default function SkillsSection() {
return (
<section
id="skills"
className="py-12 bg-gradient-to-b from-background to-muted/20"
>
<div className="container max-w-4xl mx-auto px-6 md:px-4">
<MotionWrapper>
<h2 className="text-2xl font-bold mb-8 text-center md:text-left">
🛠 Skills
</h2>
</MotionWrapper>
<motion.div
className="space-y-6"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
>
<motion.div variants={skillCategoryVariants}>
<GlassCard className="p-4">
<h3 className="text-lg font-medium mb-3 text-center md:text-left flex items-center">
<span className="mr-2 text-xl">💻</span> Programming Languages
</h3>
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
{skills.programmingLanguages.map((skill, index) => (
<SkillTag key={skill} skill={skill} index={index} />
))}
</div>
</GlassCard>
</motion.div>
<motion.div variants={skillCategoryVariants}>
<GlassCard className="p-4">
<h3 className="text-lg font-medium mb-3 text-center md:text-left flex items-center">
<span className="mr-2 text-xl">🎨</span> Frontend Development
</h3>
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
{skills.frontendDevelopment.map((skill, index) => (
<SkillTag key={skill} skill={skill} index={index} />
))}
</div>
</GlassCard>
</motion.div>
<motion.div variants={skillCategoryVariants}>
<GlassCard className="p-4">
<h3 className="text-lg font-medium mb-3 text-center md:text-left flex items-center">
<span className="mr-2 text-xl"></span> Backend Development
</h3>
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
{skills.backendDevelopment.map((skill, index) => (
<SkillTag key={skill} skill={skill} index={index} />
))}
</div>
</GlassCard>
</motion.div>
<motion.div variants={skillCategoryVariants}>
<GlassCard className="p-4">
<h3 className="text-lg font-medium mb-3 text-center md:text-left flex items-center">
<span className="mr-2 text-xl">🗄</span> Database & Storage
</h3>
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
{skills.databaseAndStorage.map((skill, index) => (
<SkillTag key={skill} skill={skill} index={index} />
))}
</div>
</GlassCard>
</motion.div>
<motion.div variants={skillCategoryVariants}>
<GlassCard className="p-4">
<h3 className="text-lg font-medium mb-3 text-center md:text-left flex items-center">
<span className="mr-2 text-xl"></span> Cloud & DevOps
</h3>
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
{skills.cloudAndDevOps.map((skill, index) => (
<SkillTag key={skill} skill={skill} index={index} />
))}
</div>
</GlassCard>
</motion.div>
<motion.div variants={skillCategoryVariants}>
<GlassCard className="p-4">
<h3 className="text-lg font-medium mb-3 text-center md:text-left flex items-center">
<span className="mr-2 text-xl">🧰</span> Tools & Services
</h3>
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
{skills.toolsAndServices.map((skill, index) => (
<SkillTag key={skill} skill={skill} index={index} />
))}
</div>
</GlassCard>
</motion.div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,76 @@
import * as React from "react";
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
interface TimelineItemProps {
title: string;
subtitle: string;
date: string;
isLast?: boolean;
index?: number;
children?: React.ReactNode;
}
export default function TimelineItem({
title,
subtitle,
date,
isLast = false,
index = 0,
children,
}: TimelineItemProps) {
return (
<motion.div
className="relative flex gap-6"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.2 }}
viewport={{ once: true, margin: "-50px" }}
>
<div className="flex flex-col items-center">
<motion.div
className="flex h-[18px] w-[18px] rounded-full border border-purple-500/50 bg-background dark:bg-muted z-10"
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
transition={{
type: "spring",
stiffness: 300,
damping: 15,
delay: index * 0.2 + 0.2,
}}
viewport={{ once: true, margin: "-50px" }}
/>
{!isLast && (
<motion.div
className="w-px grow bg-gradient-to-b from-purple-500/50 to-pink-500/30 dark:from-purple-500/30 dark:to-pink-500/10"
initial={{ height: 0 }}
whileInView={{ height: "100%" }}
transition={{ duration: 0.8, delay: index * 0.2 + 0.3 }}
viewport={{ once: true, margin: "-50px" }}
/>
)}
</div>
<div className={cn("pb-8", isLast ? "pb-0" : "")}>
<motion.div
className="flex flex-col gap-0.5"
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: index * 0.2 + 0.1 }}
viewport={{ once: true, margin: "-50px" }}
>
<h3 className="font-medium">{title}</h3>
<p className="text-sm text-muted-foreground">{subtitle}</p>
<p className="text-xs text-muted-foreground/70 mb-2">{date}</p>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.5, delay: index * 0.2 + 0.4 }}
viewport={{ once: true, margin: "-50px" }}
>
{children}
</motion.div>
</div>
</motion.div>
);
}

View File

@@ -1,31 +0,0 @@
---
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 hover:backdrop-blur-none 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-linear-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-200 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="shrink-0 rounded-2xl bg-center bg-cover aspect-video max-md:aspect-video w-2/6 max-md:w-full" />
)}
</article>

View File

@@ -1,28 +0,0 @@
---
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

@@ -1,103 +0,0 @@
---
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";
import { AstroError } from "astro/errors";
import { getCollection} from "astro:content";
const [staticData] = await getCollection('staticData');
if (!staticData) {
throw new AstroError("JSON data not found");
}
---
<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-linear-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={staticData.data.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-linear-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-linear-to-tr from-riptide-100 to-white dark:from-transparent dark:bg-linear-to-bl dark:to-transparent "
>
<a target="_blank" href={staticData.data.github}>
<div
class="p-8 h-full relative max-xl:p-5 max-md:p-4 flex flex-col gap-8 dark:before:bg-[radial-gradient(circle,rgba(13,188,130)_0,rgba(1,45,34)_100%)] before:bg-[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={staticData.data.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-linear-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-linear-to-tr from-riptide-100 to-mint-50 flex dark:bg-linear-to-r z-0 dark:overflow-hidden relative dark:from-mint-900 dark:to-mint-950 dark:before:bg-[radial-gradient(circle,rgba(13,188,130)_0,rgba(1,45,34)_100%)] before:bg-[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={staticData.data.profileAlt}
class="h-full w-full max-sm:rounded-full rounded-2xl max-sm:size-20 bg-center bg-cover"
style={`background-image: url(${staticData.data.profileImage})`}
>
<a href={staticData.data.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-linear-to-r from-riptide-400 to-mint-400 dark:from-riptide-200 dark:to-mint-400 text-transparent bg-clip-text"
>{staticData.data.profileTitle}</b
> 🚀</span
>
<Heading
text={staticData.data.profileName}
level={3}
/>
<Button
link={staticData.data.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

@@ -1,27 +0,0 @@
---
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

@@ -1,83 +0,0 @@
---
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-linear-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 sm:bg-linear-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="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

@@ -1,82 +0,0 @@
---
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/posts/"
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

@@ -1,32 +0,0 @@
---
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

@@ -1,47 +0,0 @@
---
import Social from "../ui/Social.astro";
import { Icon } from "astro-icon/components";
import { AstroError } from "astro/errors";
import { getCollection} from "astro:content";
const [staticData] = await getCollection('staticData');
if (!staticData) {
throw new AstroError("JSON data not found");
}
---
<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={staticData.data.github}
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>{staticData.data.alias}</strong>, with Astro</a
>
</div>
<div class="flex items-center justify-center gap-5" role="list" aria-label="Social media links">
<Social link={`mailto:${staticData.data.email}`} iconName={staticData.data.emailIconName} label={`Send email to ${staticData.data.email}`} />
<Social link={staticData.data.instagram} iconName={staticData.data.instagramIconName} label={`Visit ${staticData.data.alias} on Instagram`} />
<Social link={staticData.data.youtube} iconName={staticData.data.youtubeIconName} label={`Visit ${staticData.data.alias} on YouTube`} />
<Social link={staticData.data.github} iconName={staticData.data.githubIconName} label={`Visit ${staticData.data.alias} on GitHub`} />
<Social link={staticData.data.linkedin} iconName={staticData.data.linkedinIconName} label={`Visit ${staticData.data.alias} on LinkedIn`} />
</div>
</nav>
</footer>

View File

@@ -1,63 +0,0 @@
---
import Navigation from "./Navigation.astro";
import ThemeIcon from "../ui/ThemeIcon.astro";
import Social from "../ui/Social.astro";
import { Icon } from "astro-icon/components";
import { AstroError } from "astro/errors";
import { getCollection} from "astro:content";
const [staticData] = await getCollection('staticData');
if (!staticData) {
throw new AstroError("JSON data not found");
}
const currentPath = Astro.url.pathname;
const routes = ["/", "/portfolio", "/about-me", ];
// 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 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={staticData.data.github} iconName={staticData.data.githubIconName} />
<Social link={staticData.data.linkedin} iconName={staticData.data.linkedinIconName} />
</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

@@ -1,92 +0,0 @@
---
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

@@ -1,168 +0,0 @@
---
import Social from "../ui/Social.astro";
import { AstroError } from "astro/errors";
import { getCollection} from "astro:content";
const [staticData] = await getCollection('staticData');
if (!staticData) {
throw new AstroError("JSON data not found");
}
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/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={staticData.data.github} iconName={staticData.data.githubIconName} />
<Social link={staticData.data.linkedin} iconName={staticData.data.linkedinIconName} />
</div>
</nav>

View File

@@ -1,42 +0,0 @@
---
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

@@ -1,40 +0,0 @@
---
import Heading from "../ui/Heading.astro";
import Button from "../ui/Button.astro";
import { AstroError } from "astro/errors";
import { getCollection} from "astro:content";
const [staticData] = await getCollection('staticData');
if (!staticData) {
throw new AstroError("JSON data not found");
}
---
<section>
<div
class="w-full border-b bg-linear-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={staticData.data.contactSectionSubtitle}
textGradient={staticData.data.contactSectionSubtitle}
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:${staticData.data.email}`}
text={staticData.data.contactSectionButtonText}
iconName={staticData.data.contactSectionButtonIcon}
/>
</div>
</div>
</div>
</div>
</section>

View File

@@ -1,39 +0,0 @@
---
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

@@ -1,41 +0,0 @@
---
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

@@ -1,206 +0,0 @@
---
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";
import { AstroError } from "astro/errors";
import { getCollection} from "astro:content";
const [staticData] = await getCollection('staticData');
if (!staticData) {
throw new AstroError("JSON data not found");
}
---
<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-linear-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-linear-to-tr from-riptide-100 to-white p-4 transition-all hover:shadow-[0_10px_50px_rgba(13,188,130,0.2)] dark:bg-linear-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:bg-[radial-gradient(circle,rgba(13,188,130)_0,rgba(1,45,34)_100%)] before:bg-[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 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(${staticData.data.profileImage})`}
aria-label={staticData.data.profileAlt}
>
<a
href={staticData.data.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"
>
{staticData.data.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-linear-to-r from-riptide-500 to-mint-500 bg-clip-text font-black text-transparent dark:from-riptide-300 dark:to-mint-200"
>{staticData.data.profileTitle}</span
> with <span
class="bg-linear-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:" + staticData.data.email}
text={staticData.data.contactSectionButtonText}
iconName={staticData.data.emailIconName}
/>
<Button
link={staticData.data.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-linear-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-linear-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-linear-to-tr from-riptide-100 to-white dark:from-transparent dark:bg-linear-to-bl dark:to-transparent "
>
<a
target="_blank"
href={staticData.data.github}
aria-label="View this site's code repository on GitHub"
>
<div
class="relative flex h-full flex-col gap-8 p-8 dark:before:bg-[radial-gradient(circle,rgba(13,188,130)_0,rgba(1,45,34)_100%)] before:bg-[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-linear-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-linear-to-tr from-riptide-100 to-mint-100 p-6 transition hover:shadow-[0_10px_50px_rgba(13,188,130,0.2)] dark:bg-linear-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:bg-[radial-gradient(circle,rgba(13,188,130)_0,rgba(1,45,34)_100%)] before:bg-[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-linear-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-linear-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:bg-[radial-gradient(circle,rgba(13,188,130)_0,rgba(1,45,34)_100%)] before:bg-[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-40 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-linear-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-linear-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-linear-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:bg-[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={staticData.data.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

@@ -1,9 +0,0 @@
---
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

@@ -1,37 +0,0 @@
---
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

@@ -1,61 +0,0 @@
---
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-linear-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

@@ -1,32 +0,0 @@
---
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-linear-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-200 ease-in-out rounded-3xl bg-transparent backdrop-blur-lg hover:backdrop-blur-none 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:bg-[radial-gradient(circle,rgba(71,255,194)_0,rgba(0,255,191)_100%)] dark:before:bg-[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:bg-[radial-gradient(circle,rgba(0,255,217)_0,rgba(121,249,255,65%)_70%)] dark:after:bg-[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

@@ -1,54 +0,0 @@
---
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

@@ -1,166 +0,0 @@
---
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-linear-to-l
bg-linear-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-linear-to-l
bg-linear-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

@@ -1,87 +0,0 @@
---
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

@@ -1,35 +0,0 @@
---
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-linear-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

@@ -1,10 +0,0 @@
---
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

@@ -1,41 +0,0 @@
---
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

@@ -1,26 +0,0 @@
---
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

@@ -1,10 +0,0 @@
---
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

@@ -1,75 +0,0 @@
---
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>

View File

@@ -0,0 +1,56 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@@ -0,0 +1,82 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-lg font-medium leading-none tracking-tight", className)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

@@ -0,0 +1,37 @@
import * as React from "react";
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
import type { HTMLMotionProps } from "framer-motion";
interface GlassCardProps extends HTMLMotionProps<"div"> {
hoverEffect?: boolean;
}
const GlassCard = React.forwardRef<HTMLDivElement, GlassCardProps>(
({ className, hoverEffect = true, ...props }, ref) => {
return (
<motion.div
ref={ref}
className={cn(
"rounded-lg border border-border/50 bg-background/80 backdrop-blur-md backdrop-filter shadow-sm dark:bg-card/30 dark:backdrop-blur-md",
hoverEffect &&
"hover:shadow-md transition-all duration-300 ease-in-out",
className
)}
whileHover={
hoverEffect
? {
y: -5,
transition: { duration: 0.2 },
}
: undefined
}
{...props}
/>
);
}
);
GlassCard.displayName = "GlassCard";
export { GlassCard };

View File

@@ -0,0 +1,36 @@
import { Moon, Sun } from "lucide-react";
import { Button } from "./button";
import { useEffect, useState } from "react";
export default function ThemeToggle() {
const [theme, setTheme] = useState<"light" | "dark">("light");
useEffect(() => {
const isDark = document.documentElement.classList.contains("dark");
setTheme(isDark ? "dark" : "light");
}, []);
const toggleTheme = () => {
const isDark = document.documentElement.classList.contains("dark");
const newTheme = isDark ? "light" : "dark";
document.documentElement.classList.toggle("dark");
setTheme(newTheme);
};
return (
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
className="rounded-full cursor-pointer"
>
{theme === "light" ? (
<Moon className="h-5 w-5" />
) : (
<Sun className="h-5 w-5" />
)}
<span className="sr-only">Toggle theme</span>
</Button>
);
}