hangerthem.com/components/projects.tsx

312 lines
6.2 KiB
TypeScript

"use client"
import styled from "styled-components"
import { useCallback, useState } from "react"
import { CaretLeftFill, CaretRightFill, Git } from "react-bootstrap-icons"
import projects from "@/data/projects"
import Link from "next/link"
import Image from "next/image"
import { Container, Content } from "@/styles/pageStyles"
const CarouselWrapper = styled.div`
display: flex;
transition: transform 0.5s ease-in-out;
flex-direction: column;
width: 100%;
gap: 1rem;
@media (min-width: 1250px) {
flex-direction: row;
align-items: center;
}
`
const CarouselContainer = styled.div`
width: 100%;
position: relative;
`
const ProjectCard = styled.div`
width: 100%;
height: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
border-radius: 1rem;
border: 1px solid rgba(var(--white), 0.1);
transition: opacity 0.5s ease-in-out, min-width 0.5s ease-in-out;
position: relative;
h3 {
font-size: 2rem;
font-weight: 600;
@media (min-width: 1250px) {
font-size: 1.5rem;
}
}
p {
font-size: 1.2rem;
font-weight: 400;
@media (min-width: 1250px) {
font-size: 1rem;
}
}
img {
width: 100%;
height: auto;
min-height: 150px;
max-height: 200px;
object-fit: contain;
border-radius: 1rem;
}
&:not(.active) {
opacity: 0.5;
transform: scale(0.8);
}
@media (min-width: 1250px) {
padding: 2rem;
width: 400px;
height: 550px;
}
`
const NavigationButton = styled.button`
position: absolute;
display: none;
top: 50%;
transform: translateY(-50%);
border: none;
background-color: transparent;
padding: 0;
color: rgba(var(--white), 0.8);
cursor: pointer;
transition: color 0.3s;
z-index: 1;
&:hover {
color: rgb(var(--white));
}
@media (min-width: 1250px) {
display: block;
}
`
const PrevButton = styled(NavigationButton)`
left: -0.25rem;
`
const NextButton = styled(NavigationButton)`
right: -0.25rem;
`
const CarouselIndicator = styled.div`
display: flex;
justify-content: center;
gap: 0.5rem;
margin-top: 1rem;
@media (min-width: 1250px) {
margin-top: 0.5rem;
}
`
const IndicatorDot = styled.div`
width: 10px;
height: 10px;
border-radius: 50%;
cursor: pointer;
background-color: rgba(var(--white), 0.5);
&.active {
background-color: rgb(var(--white));
}
@media (min-width: 1250px) {
width: 8px;
height: 8px;
}
`
const TechStack = styled.div`
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
span {
font-size: 0.8rem;
font-weight: 400;
background-color: rgba(var(--white), 0.1);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
@media (min-width: 1250px) {
font-size: 0.7rem;
padding: 0.2rem 0.4rem;
}
}
`
const Links = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 1rem;
margin-top: auto;
`
const ViewLink = styled(Link)`
font-size: 1.2rem;
font-weight: 600;
color: rgb(var(--white));
background-color: rgba(var(--primary), 0.8);
padding: 0.5rem 1rem;
border-radius: 0.25rem;
text-decoration: none;
transition: background-color 0.2s;
&:hover {
background-color: rgb(var(--primary));
color: rgb(var(--white));
}
@media (min-width: 1250px) {
font-size: 1rem;
padding: 0.4rem 0.8rem;
}
`
const SourceLink = styled(Link)`
svg {
transition: color 0.3s;
color: rgba(var(--white), 0.8);
}
&:hover svg {
color: rgb(var(--white));
}
@media (min-width: 1250px) {
svg {
width: 25px;
height: 25px;
}
}
`
const StateIndicator = styled.div`
position: absolute;
top: 1rem;
right: 1rem;
font-size: 1.5rem;
`
const Projects = () => {
const [currentIndex, setCurrentIndex] = useState(0)
const projectCount = projects.length
const handlePrev = useCallback(() => {
setCurrentIndex(
(prevIndex) => (prevIndex - 1 + projectCount) % projectCount
)
}, [projectCount])
const handleNext = useCallback(() => {
setCurrentIndex((prevIndex) => (prevIndex + 1) % projectCount)
}, [projectCount])
const getVisibleProjects = useCallback(() => {
const prevIndex = (currentIndex - 1 + projectCount) % projectCount
const nextIndex = (currentIndex + 1) % projectCount
return [projects[prevIndex], projects[currentIndex], projects[nextIndex]]
}, [currentIndex, projectCount])
return (
<Container id="projects">
<Content>
<h2>📂 My Projects</h2>
<p>Here are some of the projects I&apos;ve worked on recently</p>
<CarouselContainer>
<CarouselWrapper>
{getVisibleProjects().map((project, index) => (
<ProjectCard
key={index}
className={index === 1 ? "active" : ""}
onClick={() => {
if (index === 0) {
handlePrev()
} else if (index === 2) {
handleNext()
}
}}
>
<StateIndicator title="Project State">
{project.state === "wip" ? "🔄" : "✅"}
</StateIndicator>
<h3>{project.title}</h3>
{project.image && (
<Image
src={project.image}
alt={project.title}
width={400}
height={200}
/>
)}
<p>{project.description}</p>
<TechStack>
{project.techStack.map((tech, index) => (
<span key={index}>{tech}</span>
))}
</TechStack>
<Links>
{project.href && (
<ViewLink
href={project.href}
target="_blank"
title="View Project"
arial-label="View Project"
>
View Project
</ViewLink>
)}
<SourceLink
href={project.source}
target="_blank"
title="View Source"
arial-label="View Source"
>
<Git size={30} />
</SourceLink>
</Links>
</ProjectCard>
))}
</CarouselWrapper>
<PrevButton onClick={handlePrev} aria-label="Previous Project">
<CaretLeftFill size={50} />
</PrevButton>
<NextButton onClick={handleNext} aria-label="Next Project">
<CaretRightFill size={50} />
</NextButton>
<CarouselIndicator>
{projects.map((_, index) => (
<IndicatorDot
key={index}
onClick={() => setCurrentIndex(index)}
className={index === currentIndex ? "active" : ""}
/>
))}
</CarouselIndicator>
</CarouselContainer>
</Content>
</Container>
)
}
export default Projects