diff --git a/src/components/ProjectsSection.module.css b/src/components/ProjectsSection.module.css index 3530cf5..746ce62 100644 --- a/src/components/ProjectsSection.module.css +++ b/src/components/ProjectsSection.module.css @@ -7,35 +7,15 @@ .headerRow { display: flex; + align-items: flex-start; justify-content: space-between; - align-items: flex-end; - margin-bottom: 40px; -} - -.controls { - display: flex; - align-items: center; - gap: 20px; -} - -.counter { - font-size: var(--font-size-body-large); - font-weight: 500; - color: var(--text-muted); -} - -.counterActive { - font-weight: 700; - color: var(--text-primary); -} - -.counterDivider { - margin: 0 4px; + margin-bottom: 44px; } .arrowButtons { display: flex; gap: 12px; + padding-top: 2px; } .navButton { @@ -44,55 +24,116 @@ justify-content: center; width: var(--size-icon-button); height: var(--size-icon-button); + padding: 0; + border: 2px solid var(--color-blue-600); border-radius: 50%; - border: 1px solid var(--border-default); - background: var(--bg-surface); - color: var(--text-primary); cursor: pointer; - transition: all 0.2s ease; + transition: color 0.2s ease, background-color 0.2s ease, transform 0.2s ease; +} + +.navButtonPrevious { + background: transparent; + color: var(--color-blue-600); +} + +.navButtonNext { + background: var(--color-blue-600); + color: var(--color-blue-white); } .navButton:hover:not(:disabled) { - border-color: var(--accent-default); - background: var(--bg-subtle); - transform: scale(1.05); + transform: translateY(-1px); +} + +.navButtonPrevious:hover:not(:disabled) { + background: var(--color-blue-200); +} + +.navButtonNext:hover:not(:disabled) { + border-color: var(--color-blue-700); + background: var(--color-blue-700); +} + +.navButton:focus-visible { + outline: 3px solid var(--focus-ring); + outline-offset: 3px; } .navButton:disabled { - opacity: 0.35; - cursor: not-allowed; + cursor: default; +} + +[data-theme="dark"] .navButtonPrevious { + border-color: var(--color-blue-white); + color: var(--color-blue-white); +} + +[data-theme="dark"] .navButtonPrevious:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.12); +} + +[data-theme="dark"] .navButtonNext { + border-color: var(--color-blue-white); + background: var(--color-blue-white); + color: var(--color-blue-950); +} + +[data-theme="dark"] .navButtonNext:hover:not(:disabled) { + background: var(--color-blue-200); } .carouselViewport { - width: 1320px; - overflow: visible; position: relative; + width: 100%; + overflow-x: auto; + overflow-y: hidden; + cursor: grab; + scrollbar-width: none; + overscroll-behavior-inline: contain; + touch-action: pan-y; + user-select: none; +} + +.carouselViewport::-webkit-scrollbar { + display: none; +} + +.carouselViewport:focus-visible { + border-radius: var(--radius-lg); + outline: 3px solid var(--focus-ring); + outline-offset: 4px; +} + +.dragging { + cursor: grabbing; } .carouselTrack { display: flex; + align-items: stretch; + width: max-content; gap: 24px; - transition: transform 0.4s cubic-bezier(0.2, 0, 0.2, 1); - will-change: transform; + padding: 4px 0 8px; } .card { - flex: 0 0 360px; - width: 360px; - background-color: var(--bg-surface); + display: flex; + flex: 0 0 var(--size-project-width); + flex-direction: column; + width: var(--size-project-width); + min-height: 480px; + overflow: hidden; border: 1px solid var(--border-default); border-radius: var(--radius-lg); - overflow: hidden; - display: flex; - flex-direction: column; - box-shadow: 0 4px 16px var(--shadow-color); - transition: all 0.25s ease; + background: var(--bg-surface); + scroll-snap-align: start; + transition: border-color 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease; } .card:hover { - border-color: var(--accent-default); - transform: translateY(-4px); - box-shadow: 0 12px 28px var(--shadow-color); + border-color: var(--border-strong); + box-shadow: 0 10px 24px var(--shadow-color); + transform: translateY(-2px); } .mediaContainer { @@ -100,62 +141,69 @@ width: 100%; height: var(--size-project-media-height); overflow: hidden; - background-color: #0c1a3a; + background: var(--color-blue-900); } .projectImage { object-fit: cover; + pointer-events: none; transition: transform 0.3s ease; } .card:hover .projectImage { - transform: scale(1.03); + transform: scale(1.02); } .cardBody { - padding: 24px; display: flex; + flex: 1; flex-direction: column; - flex-grow: 1; + padding: 24px; } .cardTitleRow { display: flex; align-items: center; - justify-content: space-between; + gap: 7px; margin-bottom: 6px; } .cardTitle { - font-size: var(--font-size-h3); - line-height: var(--line-height-h3); - font-weight: 700; color: var(--text-primary); + font-size: var(--font-size-h3); + font-weight: 700; + line-height: var(--line-height-h3); } -.linkIcon { +.titleLinkIcon { + flex: 0 0 auto; + color: var(--color-blue-500); +} + +[data-theme="dark"] .titleLinkIcon { color: var(--accent-default); } .cardDate { - font-size: var(--font-size-label-small); - line-height: var(--line-height-label-small); - font-weight: 700; - letter-spacing: var(--letter-spacing-eyebrow); - color: var(--text-muted); - text-transform: uppercase; margin-bottom: 14px; + color: var(--text-muted); + font-size: var(--font-size-label-small); + font-weight: 700; + line-height: var(--line-height-label-small); + letter-spacing: var(--letter-spacing-eyebrow); + text-transform: uppercase; } .cardDesc { - font-size: var(--font-size-body-small); - line-height: var(--line-height-body-small); - color: var(--text-secondary); - margin-bottom: 20px; display: -webkit-box; - -webkit-line-clamp: 4; - -webkit-box-orient: vertical; + margin-bottom: 20px; overflow: hidden; + color: var(--text-primary); + font-size: var(--font-size-body-small); + font-weight: 400; + line-height: var(--line-height-body-small); + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; } .tagList { @@ -166,14 +214,19 @@ } .tagBadge { - padding: 4px 12px; + padding: 5px 12px; + border: 0; border-radius: var(--radius-pill); - background-color: var(--bg-subtle); - color: var(--action-link); + background: var(--color-blue-200); + color: var(--color-blue-700); font-size: var(--font-size-label-small); + font-weight: 500; line-height: var(--line-height-label-small); - font-weight: 600; - border: 1px solid var(--border-default); +} + +[data-theme="dark"] .tagBadge { + background: var(--color-blue-800); + color: var(--color-blue-100); } .cardFooter { @@ -183,40 +236,96 @@ .caseStudyLink { display: inline-flex; align-items: center; - gap: 8px; + gap: 10px; + color: var(--color-blue-500); font-size: var(--font-size-body-small); font-weight: 600; - color: var(--action-link); - transition: gap 0.2s ease; + transition: color 0.2s ease, gap 0.2s ease; } .caseStudyLink:hover { - gap: 12px; + gap: 14px; + color: var(--color-blue-700); } -.arrowIcon { - font-size: 16px; - transition: transform 0.2s ease; +.caseStudyLink:focus-visible { + border-radius: 2px; + outline: 3px solid var(--focus-ring); + outline-offset: 4px; } -.progressTrackWrapper { +[data-theme="dark"] .caseStudyLink { + color: var(--accent-default); +} + +[data-theme="dark"] .caseStudyLink:hover { + color: var(--color-blue-100); +} + +.scrollbarWrapper { display: flex; justify-content: center; - margin-top: 48px; + margin-top: 40px; } -.progressTrack { +.scrollbar { width: 480px; - height: 4px; - background-color: var(--border-default); - border-radius: var(--radius-pill); - overflow: hidden; - position: relative; + height: 20px; + margin: 0; + appearance: none; + background: transparent; + cursor: pointer; } -.progressBar { - height: 100%; - background-color: var(--accent-default); +.scrollbar::-webkit-slider-runnable-track { + height: 6px; border-radius: var(--radius-pill); - transition: transform 0.3s ease; + background: var(--color-blue-200); +} + +.scrollbar::-webkit-slider-thumb { + width: 216px; + height: 6px; + margin-top: 0; + appearance: none; + border: 0; + border-radius: var(--radius-pill); + background: var(--color-blue-400); +} + +.scrollbar::-moz-range-track { + height: 6px; + border: 0; + border-radius: var(--radius-pill); + background: var(--color-blue-200); +} + +.scrollbar::-moz-range-thumb { + width: 216px; + height: 6px; + border: 0; + border-radius: var(--radius-pill); + background: var(--color-blue-400); +} + +.scrollbar:focus-visible { + border-radius: var(--radius-pill); + outline: 3px solid var(--focus-ring); + outline-offset: 3px; +} + +[data-theme="dark"] .scrollbar::-webkit-slider-runnable-track { + background: var(--color-blue-800); +} + +[data-theme="dark"] .scrollbar::-webkit-slider-thumb { + background: var(--color-blue-300); +} + +[data-theme="dark"] .scrollbar::-moz-range-track { + background: var(--color-blue-800); +} + +[data-theme="dark"] .scrollbar::-moz-range-thumb { + background: var(--color-blue-300); } diff --git a/src/components/ProjectsSection.tsx b/src/components/ProjectsSection.tsx index 8a90d82..83e52b0 100644 --- a/src/components/ProjectsSection.tsx +++ b/src/components/ProjectsSection.tsx @@ -1,138 +1,214 @@ 'use client'; -import React, { useState, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import Image from 'next/image'; import { ArrowLeft, ArrowRight, ArrowUpRight } from 'lucide-react'; import { projects } from '@/data/projects'; import styles from './ProjectsSection.module.css'; +const CARD_STEP = 384; + export const ProjectsSection: React.FC = () => { - const [currentIndex, setCurrentIndex] = useState(0); - const trackRef = useRef(null); + const viewportRef = useRef(null); + const dragState = useRef({ + active: false, + moved: false, + startX: 0, + startScrollLeft: 0, + }); + const [isDragging, setIsDragging] = useState(false); + const [scrollProgress, setScrollProgress] = useState(0); + const [canScrollPrev, setCanScrollPrev] = useState(false); + const [canScrollNext, setCanScrollNext] = useState(true); - const totalCards = projects.length; - // Step by 1 card, up to totalCards - 3 - const maxIndex = Math.max(0, totalCards - 3); + const syncScrollState = () => { + const viewport = viewportRef.current; + if (!viewport) return; - const handlePrev = () => { - setCurrentIndex((prev) => Math.max(0, prev - 1)); + const maxScroll = Math.max(0, viewport.scrollWidth - viewport.clientWidth); + const progress = maxScroll > 0 ? (viewport.scrollLeft / maxScroll) * 100 : 0; + + setScrollProgress(progress); + setCanScrollPrev(viewport.scrollLeft > 1); + setCanScrollNext(viewport.scrollLeft < maxScroll - 1); }; - const handleNext = () => { - setCurrentIndex((prev) => Math.min(maxIndex, prev + 1)); + useEffect(() => { + syncScrollState(); + window.addEventListener('resize', syncScrollState); + return () => window.removeEventListener('resize', syncScrollState); + }, []); + + const scrollByCard = (direction: -1 | 1) => { + viewportRef.current?.scrollBy({ + left: direction * CARD_STEP, + behavior: 'smooth', + }); }; - // Card width (360px) + gap (24px) = 384px - const translateX = currentIndex * 384; + const handlePointerDown = (event: React.PointerEvent) => { + if (event.button !== 0) return; - const progressPercent = maxIndex > 0 ? (currentIndex / maxIndex) * 100 : 0; + const viewport = viewportRef.current; + if (!viewport) return; + + dragState.current = { + active: true, + moved: false, + startX: event.clientX, + startScrollLeft: viewport.scrollLeft, + }; + viewport.setPointerCapture(event.pointerId); + setIsDragging(true); + }; + + const handlePointerMove = (event: React.PointerEvent) => { + const viewport = viewportRef.current; + if (!viewport || !dragState.current.active) return; + + const distance = event.clientX - dragState.current.startX; + if (Math.abs(distance) > 3) dragState.current.moved = true; + viewport.scrollLeft = dragState.current.startScrollLeft - distance; + }; + + const stopDragging = (event: React.PointerEvent) => { + const viewport = viewportRef.current; + if (!viewport || !dragState.current.active) return; + + dragState.current.active = false; + if (viewport.hasPointerCapture(event.pointerId)) { + viewport.releasePointerCapture(event.pointerId); + } + setIsDragging(false); + }; + + const handleClickCapture = (event: React.MouseEvent) => { + if (!dragState.current.moved) return; + + event.preventDefault(); + event.stopPropagation(); + dragState.current.moved = false; + }; + + const handleScrollbarChange = (event: React.ChangeEvent) => { + const viewport = viewportRef.current; + if (!viewport) return; + + const maxScroll = Math.max(0, viewport.scrollWidth - viewport.clientWidth); + viewport.scrollLeft = (Number(event.target.value) / 100) * maxScroll; + }; return ( -
+
- {/* Section Header with Controls */}
PROJECTS
-

Products I've Brought to Life

+

+ Products I've Brought to Life +

- {/* Pagination Counter & Navigation Arrows */} -
- - {currentIndex + 1} - / - {totalCards} - +
+ -
- - - -
+
- {/* Carousel Viewport */} -
-
- {projects.map((p) => ( -
- {/* Project Image Media */} +
+
+ {projects.map((project) => ( +
{p.title}
- {/* Project Details */}
-

{p.title}

- +

{project.title}

+
-
{p.date}
+
{project.date}
+

{project.description}

-

{p.description}

- - {/* Tech Tags */}
- {p.tags.map((tag) => ( + {project.tags.map((tag) => ( {tag} ))}
- {/* Case Study Action */}
-
+ ))}
- {/* Progress Bar Track */} -
-
-
-
+
+