refactor: 清理无用资源并更新项目配置
删除大量未使用的图标、图片和组件文件 更新.gitignore、tsconfig.json和astro配置 添加新的工具函数和UI组件 修改项目元数据和依赖项
This commit is contained in:
65
src/components/AwardsSection.tsx
Normal file
65
src/components/AwardsSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
72
src/components/EducationSection.tsx
Normal file
72
src/components/EducationSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
72
src/components/ExperienceSection.tsx
Normal file
72
src/components/ExperienceSection.tsx
Normal 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
58
src/components/Footer.tsx
Normal 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 }}
|
||||
>
|
||||
© {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>
|
||||
);
|
||||
}
|
||||
100
src/components/GlassHeader.tsx
Normal file
100
src/components/GlassHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
140
src/components/HeroSection.tsx
Normal file
140
src/components/HeroSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
41
src/components/MotionWrapper.tsx
Normal file
41
src/components/MotionWrapper.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
70
src/components/ProjectsSection.tsx
Normal file
70
src/components/ProjectsSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
148
src/components/SkillsSection.tsx
Normal file
148
src/components/SkillsSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
76
src/components/TimelineItem.tsx
Normal file
76
src/components/TimelineItem.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)]"
|
||||
>•</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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
56
src/components/ui/button.tsx
Normal file
56
src/components/ui/button.tsx
Normal 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 };
|
||||
82
src/components/ui/card.tsx
Normal file
82
src/components/ui/card.tsx
Normal 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,
|
||||
};
|
||||
37
src/components/ui/glass-card.tsx
Normal file
37
src/components/ui/glass-card.tsx
Normal 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 };
|
||||
36
src/components/ui/theme-toggle.tsx
Normal file
36
src/components/ui/theme-toggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user