feat(projects): update project links to showcase page and redesign dedicated project layout

This commit is contained in:
ssbg04 committed 2026-09-12 20:41:36 +08:00
1 parent 363fa64faf
commit 64cc41e919
3 files changed
+395 -462

No files matched your search

+49 -43
View File
@@ -1,43 +1,49 @@
<!-- - testimonial section stars add in sanity studio
- accessibility dropdown background color in light mode it has transparent
- add a photo gallery in gallery page -> uploads from sanity title(optional), photo upload/url, description (optional), category(optional) -> hover overlay blur + show title + show description + click show full image in a modal with cross button in top right -> add filter by category in top right -> display all the photos in a grid layout. -> responsive grid layout for mobile, tablet and desktop -> animation for the photo grid when loaded from the bottom
- optimize the low end mobile color theme -> dark mode, light mode -> colors, background, buttons, text, links, icons, everything minimize the blur effect
- chat bot remove from navigation bar and its page -> turn the chat bot into FAB -> keyword-matching search assistant for chat -> dont use api, dont label it as ai chat bot label it as Portfolio Assistant
- footer make the spotify like on messenger/instagram chat music disk spinning when playing if not its not sppining disk -> layout the socials that is not blocked by bring to top button
- link page -> optimize the theme colors, layout, animation for low end mobile -> make it linktree style that no header and footer
- navigation bar in mobile -> fix the background colors theme -> fix not working minimize or bring back icon
-->
<!-- - fix certifications page title and numbering it should be 3, tech stack is 2
- redesign the card of certificates large picture, badge/cert code, issuing org, and title only -> hover and when click show all the information of the certificate in a modal -> dont make verify button only on credly or other platform
- in about page it has certifications section -> remove it because it is already in certifications page -->
<!-- - in about section bento cell 2 make the image optimize load [About.tsx#L99-103](textBlock;file:///f%3A/WebDevelopment/Portfolio/src/components/About.tsx#L99-103)
- link page cv url /CV-Cris-Charles-Garcia.pdf -> available work chip fetch from sanity same from home page
- in cards of certificate/badges can be previewed if the picture is a certificate not a badge so make it large and can be previewed
- certifications page title remove the ```03 // Credentials & Badges
Certifications & Badges
Verified digital credentials from Cisco and industry partners. Each badge represents completed coursework, assessments, and demonstrated competency
<!-- - background shapes in landscape the right side shapes collides
- in portrait make the different approach of parallex and layout of the shapes -->
<!-- - Lite mode default the toggle is disabled
- hide the magnifier lens settings if detected mobile
- limit portfolio assistant pre selections, add feature that shows their ip and location when prompts like a using AI chatbot or if they try to prompt in the portfolio assistant like its a AI -->
<!-- - fix the lite mode only hero section loads when i reload that enabled lite mode
- make the UI in mobile same as on pc but when lagging notify to turn on the lite mode -->
<!-- - remove title description per sections
- in project section remove the screenshot picture frame like its a browser in localhost, just make it simple
- do not use emoji
- certifications section -> all categories use simple word and short for title category
- only fetch spotify widget when modal is open, fix recent play its pausing
- in tech stack technology add icon fontawesome and update the sanity io -->
<!-- # TECHNOLOGIES & TOOLS
- make the per categories simple and easy to read, remove some uneccessary components, remove the per skills has its box
# CONTACT SECTION
- fix the "Let's create something amazing." is under lapping to paragraph element
- in ultra fast mode the form still has label animation, fix the label to be static and dont move or animate, always show the label above the input field -->
<!-- - testimonial section stars add in sanity studio
- accessibility dropdown background color in light mode it has transparent
- add a photo gallery in gallery page -> uploads from sanity title(optional), photo upload/url, description (optional), category(optional) -> hover overlay blur + show title + show description + click show full image in a modal with cross button in top right -> add filter by category in top right -> display all the photos in a grid layout. -> responsive grid layout for mobile, tablet and desktop -> animation for the photo grid when loaded from the bottom
- optimize the low end mobile color theme -> dark mode, light mode -> colors, background, buttons, text, links, icons, everything minimize the blur effect
- chat bot remove from navigation bar and its page -> turn the chat bot into FAB -> keyword-matching search assistant for chat -> dont use api, dont label it as ai chat bot label it as Portfolio Assistant
- footer make the spotify like on messenger/instagram chat music disk spinning when playing if not its not sppining disk -> layout the socials that is not blocked by bring to top button
- link page -> optimize the theme colors, layout, animation for low end mobile -> make it linktree style that no header and footer
- navigation bar in mobile -> fix the background colors theme -> fix not working minimize or bring back icon
-->
<!-- - fix certifications page title and numbering it should be 3, tech stack is 2
- redesign the card of certificates large picture, badge/cert code, issuing org, and title only -> hover and when click show all the information of the certificate in a modal -> dont make verify button only on credly or other platform
- in about page it has certifications section -> remove it because it is already in certifications page -->
<!-- - in about section bento cell 2 make the image optimize load [About.tsx#L99-103](textBlock;file:///f%3A/WebDevelopment/Portfolio/src/components/About.tsx#L99-103)
- link page cv url /CV-Cris-Charles-Garcia.pdf -> available work chip fetch from sanity same from home page
- in cards of certificate/badges can be previewed if the picture is a certificate not a badge so make it large and can be previewed
- certifications page title remove the ```03 // Credentials & Badges
Certifications & Badges
Verified digital credentials from Cisco and industry partners. Each badge represents completed coursework, assessments, and demonstrated competency
<!-- - background shapes in landscape the right side shapes collides
- in portrait make the different approach of parallex and layout of the shapes -->
<!-- - Lite mode default the toggle is disabled
- hide the magnifier lens settings if detected mobile
- limit portfolio assistant pre selections, add feature that shows their ip and location when prompts like a using AI chatbot or if they try to prompt in the portfolio assistant like its a AI -->
<!-- - fix the lite mode only hero section loads when i reload that enabled lite mode
- make the UI in mobile same as on pc but when lagging notify to turn on the lite mode -->
<!-- - remove title description per sections
- in project section remove the screenshot picture frame like its a browser in localhost, just make it simple
- do not use emoji
- certifications section -> all categories use simple word and short for title category
- only fetch spotify widget when modal is open, fix recent play its pausing
- in tech stack technology add icon fontawesome and update the sanity io -->
<!-- # TECHNOLOGIES & TOOLS
- make the per categories simple and easy to read, remove some uneccessary components, remove the per skills has its box
# CONTACT SECTION
- fix the "Let's create something amazing." is under lapping to paragraph element
- in ultra fast mode the form still has label animation, fix the label to be static and dont move or animate, always show the label above the input field -->
<!-- Project Section
- make the inpect overview update to open dedicated show case page, use basic words for all buttons,
Project Dedicated page
- redesign the layout, remove the background border design, spread all the information, make it responsive, organize the contents -->␍
+63 -281
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import type { Project } from '../lib/data';
import ScrollReveal from './ScrollReveal';
@@ -16,32 +16,6 @@ export default function Projects({
sectionTag = '02 // Projects'
}: ProjectsProps) {
const [filter, setFilter] = useState<string>('All');
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [activeImage, setActiveImage] = useState<string>('');
// Synchronize active preview image and lock body scroll when modal opens
useEffect(() => {
if (selectedProject) {
setActiveImage(selectedProject.coverImage || '');
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [selectedProject]);
// Handle ESC key to close modal
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setSelectedProject(null);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
// Extract unique categories
const categories = Array.from(
@@ -59,11 +33,6 @@ export default function Projects({
const flagshipProject = isAllView && filteredProjects.length > 0 ? filteredProjects[0] : null;
const secondaryProjects = isAllView && filteredProjects.length > 0 ? filteredProjects.slice(1) : filteredProjects;
// Compile all images for the selected project (cover + gallery)
const modalImages = selectedProject
? [selectedProject.coverImage, ...(selectedProject.gallery || [])].filter(Boolean) as string[]
: [];
return (
<section id="projects" className="py-20 relative">
<div className="max-w-6xl mx-auto px-4 sm:px-6 relative z-10">
@@ -111,19 +80,20 @@ export default function Projects({
</ScrollReveal>
{/* ============================================================== */}
{/* FLAGSHIP HERO PROJECT (Option 1 - Full Width Showcase) */}
{/* FLAGSHIP HERO PROJECT */}
{/* ============================================================== */}
{flagshipProject && (
<ScrollReveal variant="fade-up" className="mb-8">
<article
onClick={() => setSelectedProject(flagshipProject)}
className="bento-card p-6 sm:p-8 lg:p-10 border border-border-custom hover:border-primary-custom/40 transition-all duration-300 cursor-pointer group/hero"
>
<article className="bento-card p-6 sm:p-8 lg:p-10 border border-border-custom hover:border-primary-custom/40 transition-all duration-300 group/hero">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center">
{/* Left: Project Screenshot Viewport */}
<div className="lg:col-span-7 flex flex-col justify-between">
<div className="relative aspect-[16/10] w-full rounded-2xl overflow-hidden border border-border-custom bg-muted-custom/20 shadow-lg group/mockup">
<a
href={`/projects/${flagshipProject.slug}`}
className="relative aspect-[16/10] w-full rounded-2xl overflow-hidden border border-border-custom bg-muted-custom/20 shadow-lg group/mockup block"
aria-label={`View ${flagshipProject.title}`}
>
{flagshipProject.status && (
<div className="absolute top-3.5 right-3.5 z-20 px-2.5 py-1 rounded-full bg-background-custom/85 backdrop-blur-md border border-border-custom text-[10px] font-mono font-medium text-emerald-500 dark:text-emerald-400 flex items-center gap-1.5 shadow-sm">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
@@ -143,16 +113,16 @@ export default function Projects({
<span className="font-mono text-xs text-muted-foreground-custom">Preview Interface</span>
</div>
)}
{/* Click to inspect overlay */}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover/hero:opacity-100 transition-opacity duration-300 flex items-center justify-center pointer-events-none">
{/* Hover overlay link hint */}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover/mockup:opacity-100 transition-opacity duration-300 flex items-center justify-center pointer-events-none">
<span className="px-3.5 py-1.5 rounded-full bg-background-custom/90 text-foreground-custom font-mono text-xs font-semibold backdrop-blur-sm shadow-md flex items-center gap-1.5">
<svg className="w-3.5 h-3.5 text-primary-custom" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6" />
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
</svg>
Click to View Details
View Project
</span>
</div>
</div>
</a>
{/* Highlight Impact Pills (Below screenshot) */}
{flagshipProject.highlights && flagshipProject.highlights.length > 0 && (
@@ -178,7 +148,7 @@ export default function Projects({
{/* Eyebrow & Meta */}
<div className="flex items-center justify-between gap-2 mb-3">
<span className="px-2.5 py-1 rounded-md bg-primary-custom/10 border border-primary-custom/20 text-[10px] font-mono font-semibold uppercase tracking-wider text-primary-custom">
{flagshipProject.category || 'Flagship Project'}
{flagshipProject.category || 'Featured'}
</span>
<span className="text-xs font-mono text-muted-foreground-custom">
{flagshipProject.publishedAt || '2026'}
@@ -186,9 +156,14 @@ export default function Projects({
</div>
{/* Title */}
<h3 className="text-2xl sm:text-3xl font-bold font-heading text-foreground-custom group-hover/hero:text-primary-custom transition-colors tracking-tight leading-snug">
{flagshipProject.title}
</h3>
<a
href={`/projects/${flagshipProject.slug}`}
className="block group/title"
>
<h3 className="text-2xl sm:text-3xl font-bold font-heading text-foreground-custom group-hover/title:text-primary-custom transition-colors tracking-tight leading-snug">
{flagshipProject.title}
</h3>
</a>
{/* Summary */}
<p className="text-sm text-muted-foreground-custom leading-relaxed mt-3 mb-6 font-normal">
@@ -198,16 +173,13 @@ export default function Projects({
{/* Technologies Tag Group */}
<div className="mb-6">
<span className="text-[11px] font-mono text-muted-foreground-custom block mb-2 font-medium">
Technologies Deployed
Technologies
</span>
<div className="flex flex-wrap gap-1.5">
{flagshipProject.technologies.map((tech) => (
<button
key={tech}
onClick={(e) => {
e.stopPropagation();
setFilter(tech);
}}
onClick={() => setFilter(tech)}
className="tech-tag hover:border-primary-custom cursor-pointer"
title={`Filter by ${tech}`}
>
@@ -218,34 +190,29 @@ export default function Projects({
</div>
</div>
{/* Flagship Actions Footer */}
{/* Flagship Actions Footer with basic words */}
<div className="flex flex-wrap items-center gap-3 pt-5 border-t border-border-custom">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setSelectedProject(flagshipProject);
}}
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-primary-custom text-white text-xs font-semibold hover:bg-primary-custom/90 transition-all shadow-sm hover:shadow hover:-translate-y-0.5 active:translate-y-0 cursor-pointer"
<a
href={`/projects/${flagshipProject.slug}`}
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-primary-custom text-white text-xs font-semibold hover:bg-primary-custom/90 transition-all shadow-sm hover:shadow hover:-translate-y-0.5 active:translate-y-0"
>
Inspect Overview
View Project
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
</svg>
</button>
</a>
{flagshipProject.repositoryUrl && (
<a
href={flagshipProject.repositoryUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="inline-flex items-center gap-1.5 px-3.5 py-2.5 rounded-xl border border-border-custom hover:border-primary-custom/40 text-xs font-mono font-medium text-foreground-custom hover:text-primary-custom transition-all"
>
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Source
Code
</a>
)}
@@ -254,10 +221,9 @@ export default function Projects({
href={flagshipProject.liveUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="inline-flex items-center gap-1 text-xs font-mono font-medium text-primary-custom hover:underline ml-auto"
>
Live Demo ↗
Demo ↗
</a>
)}
</div>
@@ -279,14 +245,15 @@ export default function Projects({
return (
<ScrollReveal key={project.slug} variant="fade-up" delay={idx * 80}>
<article
onClick={() => setSelectedProject(project)}
className="bento-card p-6 sm:p-7 flex flex-col justify-between h-full group hover:border-primary-custom/40 transition-all duration-300 cursor-pointer"
>
<article className="bento-card p-6 sm:p-7 flex flex-col justify-between h-full group hover:border-primary-custom/40 transition-all duration-300">
<div>
{/* Project Screenshot Viewport for Secondary Cards */}
{project.coverImage ? (
<div className="relative aspect-[16/9] w-full rounded-2xl overflow-hidden border border-border-custom bg-muted-custom/20 mb-5 group/submockup shadow-xs">
<a
href={`/projects/${project.slug}`}
className="relative aspect-[16/9] w-full rounded-2xl overflow-hidden border border-border-custom bg-muted-custom/20 mb-5 group/submockup shadow-xs block"
aria-label={`View ${project.title}`}
>
{project.status && (
<div className="absolute top-2.5 right-2.5 z-20 px-2 py-0.5 rounded-full bg-background-custom/85 backdrop-blur-md border border-border-custom text-[9px] font-mono font-medium text-emerald-500 dark:text-emerald-400 flex items-center gap-1 shadow-sm">
<span className="w-1 h-1 rounded-full bg-emerald-500 animate-pulse" />
@@ -301,12 +268,15 @@ export default function Projects({
className="w-full h-full object-cover group-hover/submockup:scale-[1.03] transition-transform duration-500 ease-out"
/>
{/* Hover overlay hint */}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center pointer-events-none">
<span className="px-3 py-1 rounded-full bg-background-custom/90 text-foreground-custom font-mono text-[11px] font-medium backdrop-blur-sm shadow-sm">
View Details ↗
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover/submockup:opacity-100 transition-opacity duration-300 flex items-center justify-center pointer-events-none">
<span className="px-3 py-1 rounded-full bg-background-custom/90 text-foreground-custom font-mono text-[11px] font-medium backdrop-blur-sm shadow-sm flex items-center gap-1.5">
<svg className="w-3 h-3 text-primary-custom" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
</svg>
View Project
</span>
</div>
</div>
</a>
) : null}
{/* Header Meta */}
@@ -327,9 +297,14 @@ export default function Projects({
</div>
{/* Title */}
<h3 className="text-xl font-bold font-heading text-foreground-custom group-hover:text-primary-custom transition-colors tracking-tight leading-snug mt-1">
{project.title}
</h3>
<a
href={`/projects/${project.slug}`}
className="block group/title"
>
<h3 className="text-xl font-bold font-heading text-foreground-custom group-hover/title:text-primary-custom transition-colors tracking-tight leading-snug mt-1">
{project.title}
</h3>
</a>
{/* Summary */}
<p className="text-sm text-muted-foreground-custom leading-relaxed mt-2.5 mb-4 font-normal">
@@ -354,10 +329,7 @@ export default function Projects({
{project.technologies.map((tech) => (
<button
key={tech}
onClick={(e) => {
e.stopPropagation();
setFilter(tech);
}}
onClick={() => setFilter(tech)}
className="tech-tag hover:border-primary-custom cursor-pointer"
title={`Filter by ${tech}`}
>
@@ -366,21 +338,17 @@ export default function Projects({
))}
</div>
{/* Action Links */}
{/* Action Links with basic words */}
<div className="flex items-center justify-between gap-3 pt-3 border-t border-border-custom">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setSelectedProject(project);
}}
className="inline-flex items-center gap-1 text-xs font-semibold text-foreground-custom hover:text-primary-custom transition-colors cursor-pointer"
<a
href={`/projects/${project.slug}`}
className="inline-flex items-center gap-1 text-xs font-semibold text-foreground-custom hover:text-primary-custom transition-colors"
>
Quick View
View Project
<svg className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
</a>
<div className="flex items-center gap-3">
{project.repositoryUrl && (
@@ -388,9 +356,8 @@ export default function Projects({
href={project.repositoryUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-xs font-mono text-muted-foreground-custom hover:text-foreground-custom transition-colors inline-flex items-center gap-1"
title="View source repository"
title="View source code"
>
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
@@ -404,7 +371,6 @@ export default function Projects({
href={project.liveUrl}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-xs font-mono font-medium text-primary-custom hover:underline"
>
Demo ↗
@@ -430,7 +396,7 @@ export default function Projects({
onClick={() => setFilter('All')}
className="px-4 py-2 rounded-xl bg-primary-custom text-white text-xs font-semibold cursor-pointer"
>
Reset Filters
Reset
</button>
</div>
)}
@@ -453,7 +419,7 @@ export default function Projects({
href="/projects"
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-background-custom border border-border-custom hover:border-primary-custom/40 text-foreground-custom text-xs font-semibold hover:text-primary-custom transition-all shadow-xs hover:shadow whitespace-nowrap"
>
Explore Full Directory
All Projects
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
</svg>
@@ -463,190 +429,6 @@ export default function Projects({
)}
</div>
{/* ============================================================== */}
{/* SPLIT MODAL: Left Screenshot Overview | Right Project Details */}
{/* ============================================================== */}
{selectedProject && (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-5 md:p-8 bg-black/80 backdrop-blur-md animate-modal-fade"
onClick={() => setSelectedProject(null)}
role="dialog"
aria-modal="true"
aria-label={`${selectedProject.title} Details`}
>
<div
className="relative w-full max-w-5xl max-h-[90vh] bg-background-custom border border-border-custom shadow-2xl rounded-2xl md:rounded-3xl overflow-hidden flex flex-col md:flex-row animate-modal-scale"
onClick={(e) => e.stopPropagation()}
>
{/* Close button (top right) */}
<button
onClick={() => setSelectedProject(null)}
className="absolute top-4 right-4 z-30 w-8 h-8 rounded-full bg-background-custom/90 border border-border-custom hover:border-primary-custom/40 text-foreground-custom flex items-center justify-center transition-all cursor-pointer shadow-md hover:scale-105 active:scale-95"
aria-label="Close modal"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/* LEFT COLUMN: Screenshot Overview & Gallery */}
<div className="w-full md:w-7/12 bg-muted-custom/25 border-b md:border-b-0 md:border-r border-border-custom p-5 sm:p-6 flex flex-col justify-between overflow-y-auto max-h-[45vh] md:max-h-[90vh]">
<div>
{/* Clean Project Screenshot Frame */}
<div className="relative aspect-[16/10] w-full rounded-2xl overflow-hidden border border-border-custom bg-black/5 mb-4 shadow-md">
{selectedProject.status && (
<div className="absolute top-3 right-3 z-20 px-2.5 py-1 rounded-full bg-background-custom/85 backdrop-blur-md border border-border-custom text-[10px] font-mono font-medium text-emerald-500 dark:text-emerald-400 flex items-center gap-1.5 shadow-sm">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
<span>{selectedProject.status}</span>
</div>
)}
{activeImage ? (
<img
src={activeImage}
alt={selectedProject.title}
className="w-full h-full object-cover transition-all duration-300"
/>
) : (
<div className="w-full h-full flex items-center justify-center p-6 text-center text-muted-foreground-custom font-mono text-xs">
No Screenshot Preview Available
</div>
)}
</div>
{/* Gallery Thumbnails Carousel if multiple images exist */}
{modalImages.length > 1 && (
<div className="mt-3">
<span className="text-[10px] font-mono uppercase tracking-wider text-muted-foreground-custom block mb-2">
Gallery Previews ({modalImages.length})
</span>
<div className="flex items-center gap-2 overflow-x-auto pb-1">
{modalImages.map((imgUrl, i) => (
<button
key={i}
onClick={() => setActiveImage(imgUrl)}
className={`relative w-16 h-11 rounded-lg overflow-hidden border-2 flex-shrink-0 transition-all cursor-pointer ${
activeImage === imgUrl
? 'border-primary-custom scale-105 shadow-md'
: 'border-border-custom opacity-70 hover:opacity-100'
}`}
>
<img src={imgUrl} alt={`Thumbnail ${i + 1}`} className="w-full h-full object-cover" />
</button>
))}
</div>
</div>
)}
</div>
{/* Bottom metadata tags */}
<div className="mt-4 pt-3 border-t border-border-custom/50 flex items-center justify-between text-xs text-muted-foreground-custom">
<span className="font-mono text-[11px]">Published: {selectedProject.publishedAt || '2026'}</span>
<span className="font-mono text-[11px] text-primary-custom">{selectedProject.order ? `Order #${selectedProject.order}` : ''}</span>
</div>
</div>
{/* RIGHT COLUMN: Project Information Details */}
<div className="w-full md:w-5/12 p-6 sm:p-8 flex flex-col justify-between overflow-y-auto max-h-[50vh] md:max-h-[90vh]">
<div>
{/* Header Category & Published Year */}
<div className="flex items-center justify-between gap-2 mb-3">
<span className="px-2.5 py-1 rounded-md bg-primary-custom/10 border border-primary-custom/20 text-[10px] font-mono font-semibold uppercase tracking-wider text-primary-custom">
{selectedProject.category || 'Project Showcase'}
</span>
<span className="text-xs font-mono text-muted-foreground-custom">
{selectedProject.publishedAt || '2026'}
</span>
</div>
{/* Title */}
<h3 className="text-2xl sm:text-3xl font-bold font-heading text-foreground-custom tracking-tight leading-snug">
{selectedProject.title}
</h3>
{/* Detailed Description */}
<div className="text-sm text-muted-foreground-custom leading-relaxed mt-4 mb-6">
<p>{selectedProject.description || selectedProject.summary}</p>
</div>
{/* Key Highlights / Impact Metrics */}
{selectedProject.highlights && selectedProject.highlights.length > 0 && (
<div className="mb-6">
<span className="text-[11px] font-mono uppercase tracking-wider text-muted-foreground-custom block mb-2 font-medium">
Key Architectural Highlights
</span>
<div className="flex flex-wrap gap-1.5">
{selectedProject.highlights.map((h) => (
<span key={h} className="impact-pill text-[10px] py-1 px-2.5">
<svg className="w-3 h-3 text-primary-custom flex-shrink-0" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
{h}
</span>
))}
</div>
</div>
)}
{/* Technologies Stack */}
<div className="mb-6">
<span className="text-[11px] font-mono uppercase tracking-wider text-muted-foreground-custom block mb-2 font-medium">
Technologies Stack
</span>
<div className="flex flex-wrap gap-1.5">
{selectedProject.technologies.map((tech) => (
<span key={tech} className="tech-tag text-xs">
{tech}
</span>
))}
</div>
</div>
</div>
{/* Action Buttons Footer */}
<div className="pt-5 border-t border-border-custom space-y-3">
<div className="flex items-center gap-2.5">
{selectedProject.liveUrl && (
<a
href={selectedProject.liveUrl}
target="_blank"
rel="noopener noreferrer"
className="flex-1 py-2.5 px-4 rounded-xl bg-primary-custom text-white text-xs font-semibold hover:bg-primary-custom/90 transition-all text-center shadow-sm flex items-center justify-center gap-1.5"
>
Launch Demo
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
</a>
)}
{selectedProject.repositoryUrl && (
<a
href={selectedProject.repositoryUrl}
target="_blank"
rel="noopener noreferrer"
className="flex-1 py-2.5 px-4 rounded-xl border border-border-custom hover:border-primary-custom/40 text-xs font-mono font-medium text-foreground-custom hover:text-primary-custom transition-all text-center flex items-center justify-center gap-1.5"
>
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Repository
</a>
)}
</div>
<a
href={`/projects/${selectedProject.slug}`}
className="block w-full py-2 text-center text-xs font-mono text-muted-foreground-custom hover:text-primary-custom transition-colors"
>
Open Dedicated Showcase Page →
</a>
</div>
</div>
</div>
</div>
)}
</section>
);
}
+283 -138
View File
@@ -6,13 +6,18 @@ import { getProjects, getSiteSettings, getSocialLinks } from '../../lib/data';
const { slug } = Astro.params;
const allProjects = await getProjects();
const project = allProjects.find(p => p.slug === slug);
const currentIndex = allProjects.findIndex(p => p.slug === slug);
const project = allProjects[currentIndex];
const settings = await getSiteSettings();
const socialLinks = await getSocialLinks();
if (!project) {
return Astro.redirect('/404');
}
// Adjacent projects for bottom navigation
const prevProject = currentIndex > 0 ? allProjects[currentIndex - 1] : null;
const nextProject = currentIndex < allProjects.length - 1 ? allProjects[currentIndex + 1] : null;
---
<Layout
@@ -22,164 +27,304 @@ if (!project) {
>
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
<main class="flex-1 pt-6 sm:pt-10 pb-16 relative overflow-hidden">
<!-- Background elements -->
<div className="absolute inset-0 z-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 left-1/4 w-[500px] h-[500px] rounded-full bg-primary-custom/5 blur-3xl" />
<div className="absolute bottom-20 right-1/4 w-80 h-80 rounded-full bg-secondary-custom/5 blur-3xl" />
<main class="flex-1 pt-6 sm:pt-10 pb-20 relative overflow-hidden">
<!-- Subtle ambient background glows (no enclosing border) -->
<div class="absolute inset-0 z-0 overflow-hidden pointer-events-none">
<div class="absolute -top-32 left-1/3 w-[600px] h-[500px] rounded-full bg-primary-custom/5 blur-3xl"></div>
<div class="absolute top-1/2 right-10 w-96 h-96 rounded-full bg-secondary-custom/5 blur-3xl"></div>
</div>
<div class="container mx-auto px-6 max-w-3xl relative z-10">
<!-- Back to projects -->
<a
href="/projects"
class="inline-flex items-center gap-1.5 text-xs font-bold text-muted-foreground-custom hover:text-primary-custom transition-colors mb-10"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
Back to projects
</a>
<div class="container mx-auto px-4 sm:px-6 lg:px-8 max-w-6xl relative z-10">
{/* Top Navigation & Meta Breadcrumb */}
<div class="flex flex-wrap items-center justify-between gap-4 mb-8">
<a
href="/projects"
class="inline-flex items-center gap-2 text-xs font-mono font-medium text-muted-foreground-custom hover:text-foreground-custom transition-colors group"
>
<svg class="w-4 h-4 group-hover:-translate-x-1 transition-transform" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
Back to Projects
</a>
<article class="glass-card p-8 md:p-12 rounded-[32px] border border-border/20 shadow-2xl relative overflow-hidden">
{/* Header banner */}
<div class="absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r from-primary-custom via-secondary-custom to-primary-custom" />
<div class="flex items-center justify-between mb-4">
<span class="text-[10px] uppercase tracking-widest font-bold text-primary-custom bg-primary-custom/10 px-3 py-1 rounded-full">
Project Showcase
</span>
<span class="text-xs font-semibold text-muted-foreground-custom">
Published: {project.publishedAt}
</span>
<div class="flex items-center gap-2.5">
{project.category && (
<span class="text-xs font-mono font-semibold text-primary-custom bg-primary-custom/10 border border-primary-custom/20 px-3 py-1 rounded-full">
{project.category}
</span>
)}
{project.status && (
<span class="text-xs font-mono font-medium text-emerald-500 dark:text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-3 py-1 rounded-full inline-flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
{project.status}
</span>
)}
</div>
</div>
<h1 class="text-3xl md:text-5xl font-bold font-heading mb-6 text-foreground-custom text-glow">
{/* Project Hero Header */}
<header class="mb-10">
<h1 class="text-3xl sm:text-5xl lg:text-6xl font-extrabold font-heading text-foreground-custom tracking-tight leading-[1.1] mb-6">
{project.title}
</h1>
<p class="text-base md:text-lg font-medium text-muted-foreground-custom/95 leading-relaxed mb-8 border-l-2 border-primary-custom/30 pl-4 italic">
<p class="text-base sm:text-lg lg:text-xl text-muted-foreground-custom leading-relaxed max-w-4xl font-normal mb-8">
{project.summary}
</p>
{/* Project Cover & Gallery */}
{project.coverImage ? (
<div class="aspect-video w-full rounded-2xl overflow-hidden border border-border-custom mb-10 shadow-lg relative group">
<img
src={project.coverImage}
alt={project.title}
class="w-full h-full object-cover group-hover:scale-[1.01] transition-transform duration-500"
/>
{project.status && (
<div class="absolute top-4 left-4 px-3 py-1 rounded-full bg-black/70 backdrop-blur-md text-xs font-mono text-emerald-400 flex items-center gap-1.5 shadow">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
{project.status}
</div>
)}
</div>
) : (
<div class="aspect-video w-full rounded-2xl bg-gradient-to-tr from-primary-custom/10 to-secondary-custom/10 border border-border/10 flex items-center justify-center p-6 mb-10 shadow-inner group overflow-hidden">
<div class="text-center group-hover:scale-105 transition-transform duration-300">
<div class="w-16 h-16 rounded-full bg-background-custom/80 flex items-center justify-center text-primary-custom mx-auto mb-4 border border-border/20 shadow">
<svg class="w-8 h-8" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M6.429 9.75 2.25 12l4.179 2.25m11.142 0L21.75 12l-4.179-2.25M12 21.75V2.25" />
</svg>
</div>
<span class="text-xs font-semibold text-muted-foreground-custom/90">{project.title} Interface</span>
</div>
</div>
)}
{/* Gallery images if present */}
{project.gallery && project.gallery.length > 0 && (
<div class="mb-10">
<h3 class="text-xs font-bold text-muted-foreground-custom uppercase tracking-wider mb-4">
Project Gallery &amp; Previews
</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
{project.gallery.map((imgUrl, i) => (
<div key={i} class="rounded-xl overflow-hidden border border-border-custom aspect-video shadow-xs">
<img src={imgUrl} alt={`${project.title} screenshot ${i+1}`} class="w-full h-full object-cover" />
</div>
))}
</div>
</div>
)}
<div class="prose dark:prose-invert max-w-none text-sm md:text-base text-muted-foreground-custom/90 leading-relaxed mb-10">
<h3 class="text-lg font-bold text-foreground-custom mb-3">Project Description</h3>
<p>{project.description}</p>
</div>
{/* Key Highlights / Impact */}
{project.highlights && project.highlights.length > 0 && (
<div class="mb-8 pt-6 border-t border-border/10">
<h3 class="text-xs font-bold text-muted-foreground-custom uppercase tracking-wider mb-3">
Key Architectural Highlights &amp; Metrics
</h3>
<div class="flex flex-wrap gap-2">
{project.highlights.map((h) => (
<span
key={h}
class="impact-pill"
>
<svg class="w-3 h-3 text-primary-custom flex-shrink-0" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
{h}
</span>
))}
</div>
</div>
)}
{/* Tech Stack List */}
<div class="mb-10 pt-6 border-t border-border/10">
<h3 class="text-xs font-bold text-muted-foreground-custom uppercase tracking-wider mb-4">
Technologies Employed
</h3>
<div class="flex flex-wrap gap-2">
{project.technologies.map((t) => (
<span
key={t}
class="text-xs font-semibold px-3.5 py-1.5 rounded-xl bg-muted-custom/45 text-muted-foreground-custom border border-border/10 font-mono"
>
{t}
</span>
))}
</div>
</div>
{/* Launch Links */}
<div class="flex flex-col sm:flex-row items-center gap-4 pt-6 border-t border-border/10">
{project.repositoryUrl && (
<a
href={project.repositoryUrl}
target="_blank"
rel="noopener noreferrer"
class="w-full sm:w-auto px-6 py-3.5 rounded-xl glass-card text-foreground-custom font-semibold text-xs border border-border/15 hover:bg-muted-custom/30 hover:scale-[1.01] active:scale-[0.99] transition-transform flex items-center justify-center gap-2"
>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Source Repository
</a>
)}
{/* Primary Quick Action Buttons */}
<div class="flex flex-wrap items-center gap-3">
{project.liveUrl && (
<a
href={project.liveUrl}
target="_blank"
rel="noopener noreferrer"
class="w-full sm:w-auto px-6 py-3.5 rounded-xl bg-primary-custom text-primary-foreground font-semibold text-xs shadow-md hover:scale-[1.01] active:scale-[0.99] transition-transform flex items-center justify-center gap-2 sm:ml-auto"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-primary-custom text-white text-xs sm:text-sm font-semibold hover:bg-primary-custom/90 transition-all shadow-sm hover:shadow hover:-translate-y-0.5 active:translate-y-0"
>
Launch Live Demo
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
Demo
<svg class="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
</a>
)}
{project.repositoryUrl && (
<a
href={project.repositoryUrl}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl border border-border-custom hover:border-primary-custom/40 bg-card-custom text-foreground-custom text-xs sm:text-sm font-mono font-medium hover:text-primary-custom transition-all"
>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Code
</a>
)}
</div>
</article>
</header>
{/* Featured Hero Cover Image Viewport */}
{project.coverImage && (
<div class="relative aspect-[16/9] w-full rounded-2xl md:rounded-3xl overflow-hidden border border-border-custom bg-muted-custom/20 mb-12 shadow-xl group">
<img
src={project.coverImage}
alt={project.title}
class="w-full h-full object-cover group-hover:scale-[1.01] transition-transform duration-500"
/>
{project.status && (
<div class="absolute top-4 left-4 px-3 py-1 rounded-full bg-background-custom/85 backdrop-blur-md border border-border-custom text-xs font-mono font-medium text-emerald-500 dark:text-emerald-400 flex items-center gap-1.5 shadow-sm">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
{project.status}
</div>
)}
</div>
)}
{/* Spread Multi-Column Responsive Layout */}
<div class="grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-12 items-start">
{/* LEFT / MAIN COLUMN (8 cols): Overview, Highlights, Gallery */}
<div class="lg:col-span-8 space-y-12">
{/* Section: Overview / Description */}
<section>
<h2 class="text-xl sm:text-2xl font-bold font-heading text-foreground-custom mb-4 flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-primary-custom" />
Overview
</h2>
<div class="text-base sm:text-lg text-muted-foreground-custom leading-relaxed space-y-4 font-normal">
<p>{project.description || project.summary}</p>
</div>
</section>
{/* Section: Key Highlights & Features */}
{project.highlights && project.highlights.length > 0 && (
<section>
<h2 class="text-xl sm:text-2xl font-bold font-heading text-foreground-custom mb-6 flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-primary-custom" />
Key Highlights
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
{project.highlights.map((highlight) => (
<div
key={highlight}
class="p-4 sm:p-5 rounded-2xl border border-border-custom bg-card-custom hover:border-primary-custom/40 transition-all flex items-start gap-3.5 shadow-xs"
>
<div class="w-6 h-6 rounded-lg bg-primary-custom/10 text-primary-custom flex items-center justify-center flex-shrink-0 mt-0.5">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
</div>
<div>
<p class="text-sm font-semibold text-foreground-custom leading-snug">
{highlight}
</p>
</div>
</div>
))}
</div>
</section>
)}
{/* Section: Gallery Previews */}
{project.gallery && project.gallery.length > 0 && (
<section>
<h2 class="text-xl sm:text-2xl font-bold font-heading text-foreground-custom mb-6 flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-primary-custom" />
Gallery &amp; Screenshots
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
{project.gallery.map((imgUrl, i) => (
<div
key={i}
class="relative aspect-[16/10] rounded-2xl overflow-hidden border border-border-custom bg-muted-custom/20 shadow-sm group"
>
<img
src={imgUrl}
alt={`${project.title} screenshot ${i + 1}`}
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
loading="lazy"
/>
</div>
))}
</div>
</section>
)}
</div>
{/* RIGHT / SIDEBAR COLUMN (4 cols): Sticky Specifications & Tech Stack */}
<aside class="lg:col-span-4 space-y-6 lg:sticky lg:top-24">
{/* Project Details Specifications Card */}
<div class="bento-card p-6 border border-border-custom space-y-5">
<h3 class="text-xs font-mono uppercase tracking-wider text-muted-foreground-custom font-semibold">
Project Details
</h3>
<div class="divide-y divide-border-custom/50 text-sm">
{project.category && (
<div class="flex items-center justify-between py-2.5">
<span class="text-muted-foreground-custom text-xs font-mono">Category</span>
<span class="text-foreground-custom font-medium text-xs text-right">{project.category}</span>
</div>
)}
{project.status && (
<div class="flex items-center justify-between py-2.5">
<span class="text-muted-foreground-custom text-xs font-mono">Status</span>
<span class="text-emerald-500 dark:text-emerald-400 font-mono text-xs font-semibold">{project.status}</span>
</div>
)}
{project.publishedAt && (
<div class="flex items-center justify-between py-2.5">
<span class="text-muted-foreground-custom text-xs font-mono">Year</span>
<span class="text-foreground-custom font-mono text-xs">{project.publishedAt}</span>
</div>
)}
<div class="flex items-center justify-between py-2.5">
<span class="text-muted-foreground-custom text-xs font-mono">Type</span>
<span class="text-foreground-custom font-medium text-xs">Software Application</span>
</div>
</div>
</div>
{/* Technologies Used Card */}
<div class="bento-card p-6 border border-border-custom space-y-4">
<h3 class="text-xs font-mono uppercase tracking-wider text-muted-foreground-custom font-semibold">
Technologies Used
</h3>
<div class="flex flex-wrap gap-2">
{project.technologies.map((t) => (
<span
key={t}
class="px-3 py-1.5 rounded-lg bg-muted-custom/40 border border-border-custom text-xs font-mono font-medium text-foreground-custom hover:border-primary-custom/40 transition-colors"
>
{t}
</span>
))}
</div>
</div>
{/* Action Buttons in Sidebar */}
<div class="bento-card p-6 border border-border-custom space-y-3">
<h3 class="text-xs font-mono uppercase tracking-wider text-muted-foreground-custom font-semibold mb-3">
Links
</h3>
{project.liveUrl && (
<a
href={project.liveUrl}
target="_blank"
rel="noopener noreferrer"
class="w-full py-2.5 px-4 rounded-xl bg-primary-custom text-white text-xs font-semibold hover:bg-primary-custom/90 transition-all text-center flex items-center justify-center gap-2 shadow-sm"
>
Demo
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
</a>
)}
{project.repositoryUrl && (
<a
href={project.repositoryUrl}
target="_blank"
rel="noopener noreferrer"
class="w-full py-2.5 px-4 rounded-xl border border-border-custom hover:border-primary-custom/40 text-xs font-mono font-medium text-foreground-custom hover:text-primary-custom transition-all text-center flex items-center justify-center gap-2"
>
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Code
</a>
)}
</div>
</aside>
</div>
{/* Bottom Adjacent Navigation Strip */}
<div class="border-t border-border-custom mt-16 pt-8 flex flex-col sm:flex-row items-center justify-between gap-4">
{prevProject ? (
<a
href={`/projects/${prevProject.slug}`}
class="inline-flex items-center gap-2 text-xs font-mono text-muted-foreground-custom hover:text-foreground-custom transition-colors group"
>
<svg class="w-4 h-4 group-hover:-translate-x-1 transition-transform" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
<span>Previous Project: <strong class="text-foreground-custom font-semibold">{prevProject.title}</strong></span>
</a>
) : (
<div />
)}
<a
href="/projects"
class="px-4 py-2 rounded-xl bg-card-custom border border-border-custom hover:border-primary-custom/40 text-xs font-mono font-medium text-muted-foreground-custom hover:text-foreground-custom transition-all"
>
Back to Projects
</a>
{nextProject ? (
<a
href={`/projects/${nextProject.slug}`}
class="inline-flex items-center gap-2 text-xs font-mono text-muted-foreground-custom hover:text-foreground-custom transition-colors group sm:ml-auto"
>
<span>Next Project: <strong class="text-foreground-custom font-semibold">{nextProject.title}</strong></span>
<svg class="w-4 h-4 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
</svg>
</a>
) : (
<div />
)}
</div>
</div>
</main>