118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
'use client';
|
|
import { motion } from 'framer-motion';
|
|
import Image from 'next/image';
|
|
import { projects } from '@/content/projects';
|
|
import styles from './Projects.module.scss';
|
|
import { SiGithub } from 'react-icons/si';
|
|
import { ArrowUpRight } from 'lucide-react';
|
|
|
|
export default function Projects() {
|
|
const featured = projects.find(p => p.featured);
|
|
const rest = projects.filter(p => !p.featured);
|
|
const hasProjects = projects.length > 0;
|
|
|
|
return (
|
|
<section className={styles.section} id="projects">
|
|
<div className={styles.container}>
|
|
<motion.header
|
|
className={styles.header}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<h2 className={styles.heading}>Selected Work<span className={styles.dot}>.</span></h2>
|
|
<p className={styles.subtitle}>
|
|
{hasProjects ? 'A selection of recent projects and side work.' : 'Projects will be added here soon.'}
|
|
</p>
|
|
</motion.header>
|
|
|
|
{!hasProjects && (
|
|
<motion.div
|
|
className={styles.empty}
|
|
initial={{ opacity: 0, y: 16 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5, delay: 0.1 }}
|
|
>
|
|
<p className={styles.emptyText}>Nothing here yet — check back later or get in touch.</p>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Featured Project */}
|
|
{hasProjects && featured && (
|
|
<motion.div
|
|
className={styles.featured}
|
|
initial={{ opacity: 0, y: 28 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.6, ease: [0.22, 0.61, 0.36, 1] }}
|
|
>
|
|
{featured.image && (
|
|
<div className={styles.featuredImage}>
|
|
<Image src={featured.image} alt={featured.title} fill style={{ objectFit: 'cover' }} />
|
|
</div>
|
|
)}
|
|
<div className={styles.featuredContent}>
|
|
<h3>{featured.title}</h3>
|
|
<p>{featured.description}</p>
|
|
<div className={styles.links}>
|
|
{featured.liveUrl && (
|
|
<a href={featured.liveUrl} target="_blank" rel="noopener noreferrer" className={styles.link}>
|
|
Live <ArrowUpRight size={14} />
|
|
</a>
|
|
)}
|
|
{featured.githubUrl && (
|
|
<a href={featured.githubUrl} target="_blank" rel="noopener noreferrer" className={styles.linkGhost}>
|
|
<SiGithub size={15} /> GitHub
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Project Grid */}
|
|
{hasProjects && (
|
|
<motion.div
|
|
className={styles.grid}
|
|
initial="hidden"
|
|
whileInView="visible"
|
|
viewport={{ once: true, margin: '-20px' }}
|
|
variants={{ visible: { transition: { staggerChildren: 0.08, delayChildren: 0.05 } } }}
|
|
>
|
|
{rest.map((project, i) => (
|
|
<motion.article
|
|
key={project.id}
|
|
className={styles.card}
|
|
variants={{
|
|
hidden: { opacity: 0, y: 20 },
|
|
visible: { opacity: 1, y: 0 },
|
|
}}
|
|
transition={{ duration: 0.45, ease: [0.22, 0.61, 0.36, 1] }}
|
|
whileHover={{ y: -5, transition: { duration: 0.2 } }}
|
|
>
|
|
<h3 className={styles.cardTitle}>{project.title}</h3>
|
|
<p className={styles.cardDesc}>{project.description}</p>
|
|
<div className={styles.cardLinks}>
|
|
{project.liveUrl && (
|
|
<a href={project.liveUrl} target="_blank" rel="noopener noreferrer" className={styles.link}>
|
|
View <ArrowUpRight size={13} />
|
|
</a>
|
|
)}
|
|
{project.githubUrl && (
|
|
<a href={project.githubUrl} target="_blank" rel="noopener noreferrer" className={styles.linkGhost}>
|
|
<SiGithub size={14} />
|
|
</a>
|
|
)}
|
|
</div>
|
|
</motion.article>
|
|
))}
|
|
</motion.div>
|
|
)}
|
|
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|