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 <!-- - testimonial section stars add in sanity studio
- accessibility dropdown background color in light mode it has transparent - 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 - 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 - 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 - 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 - 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 - 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 - 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 <!-- - 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 - 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 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) <!-- - 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 - 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 - 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 page title remove the ```03 // Credentials & Badges
Certifications & Badges Certifications & Badges
Verified digital credentials from Cisco and industry partners. Each badge represents completed coursework, assessments, and demonstrated competency 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 <!-- - background shapes in landscape the right side shapes collides
- in portrait make the different approach of parallex and layout of the shapes --> - in portrait make the different approach of parallex and layout of the shapes -->
<!-- - Lite mode default the toggle is disabled <!-- - Lite mode default the toggle is disabled
- hide the magnifier lens settings if detected mobile - 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 --> - 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 <!-- - 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 --> - make the UI in mobile same as on pc but when lagging notify to turn on the lite mode -->
<!-- - remove title description per sections <!-- - remove title description per sections
- in project section remove the screenshot picture frame like its a browser in localhost, just make it simple - in project section remove the screenshot picture frame like its a browser in localhost, just make it simple
- do not use emoji - do not use emoji
- certifications section -> all categories use simple word and short for title category - 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 - 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 --> - in tech stack technology add icon fontawesome and update the sanity io -->
<!-- # TECHNOLOGIES & TOOLS <!-- # TECHNOLOGIES & TOOLS
- make the per categories simple and easy to read, remove some uneccessary components, remove the per skills has its box - make the per categories simple and easy to read, remove some uneccessary components, remove the per skills has its box
# CONTACT SECTION # CONTACT SECTION
- fix the "Let's create something amazing." is under lapping to paragraph element - 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 --> - 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 type { Project } from '../lib/data';
import ScrollReveal from './ScrollReveal'; import ScrollReveal from './ScrollReveal';
@@ -16,32 +16,6 @@ export default function Projects({
sectionTag = '02 // Projects' sectionTag = '02 // Projects'
}: ProjectsProps) { }: ProjectsProps) {
const [filter, setFilter] = useState<string>('All'); 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 // Extract unique categories
const categories = Array.from( const categories = Array.from(
@@ -59,11 +33,6 @@ export default function Projects({
const flagshipProject = isAllView && filteredProjects.length > 0 ? filteredProjects[0] : null; const flagshipProject = isAllView && filteredProjects.length > 0 ? filteredProjects[0] : null;
const secondaryProjects = isAllView && filteredProjects.length > 0 ? filteredProjects.slice(1) : filteredProjects; 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 ( return (
<section id="projects" className="py-20 relative"> <section id="projects" className="py-20 relative">
<div className="max-w-6xl mx-auto px-4 sm:px-6 relative z-10"> <div className="max-w-6xl mx-auto px-4 sm:px-6 relative z-10">
@@ -111,19 +80,20 @@ export default function Projects({
</ScrollReveal> </ScrollReveal>
{/* ============================================================== */} {/* ============================================================== */}
{/* FLAGSHIP HERO PROJECT (Option 1 - Full Width Showcase) */} {/* FLAGSHIP HERO PROJECT */}
{/* ============================================================== */} {/* ============================================================== */}
{flagshipProject && ( {flagshipProject && (
<ScrollReveal variant="fade-up" className="mb-8"> <ScrollReveal variant="fade-up" className="mb-8">
<article <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">
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"
>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center">
{/* Left: Project Screenshot Viewport */} {/* Left: Project Screenshot Viewport */}
<div className="lg:col-span-7 flex flex-col justify-between"> <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 && ( {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"> <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" /> <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> <span className="font-mono text-xs text-muted-foreground-custom">Preview Interface</span>
</div> </div>
)} )}
{/* Click to inspect overlay */} {/* Hover overlay link hint */}
<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"> <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"> <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"> <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> </svg>
Click to View Details View Project
</span> </span>
</div> </div>
</div> </a>
{/* Highlight Impact Pills (Below screenshot) */} {/* Highlight Impact Pills (Below screenshot) */}
{flagshipProject.highlights && flagshipProject.highlights.length > 0 && ( {flagshipProject.highlights && flagshipProject.highlights.length > 0 && (
@@ -178,7 +148,7 @@ export default function Projects({
{/* Eyebrow & Meta */} {/* Eyebrow & Meta */}
<div className="flex items-center justify-between gap-2 mb-3"> <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"> <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>
<span className="text-xs font-mono text-muted-foreground-custom"> <span className="text-xs font-mono text-muted-foreground-custom">
{flagshipProject.publishedAt || '2026'} {flagshipProject.publishedAt || '2026'}
@@ -186,9 +156,14 @@ export default function Projects({
</div> </div>
{/* Title */} {/* 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"> <a
{flagshipProject.title} href={`/projects/${flagshipProject.slug}`}
</h3> 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 */} {/* Summary */}
<p className="text-sm text-muted-foreground-custom leading-relaxed mt-3 mb-6 font-normal"> <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 */} {/* Technologies Tag Group */}
<div className="mb-6"> <div className="mb-6">
<span className="text-[11px] font-mono text-muted-foreground-custom block mb-2 font-medium"> <span className="text-[11px] font-mono text-muted-foreground-custom block mb-2 font-medium">
Technologies Deployed Technologies
</span> </span>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{flagshipProject.technologies.map((tech) => ( {flagshipProject.technologies.map((tech) => (
<button <button
key={tech} key={tech}
onClick={(e) => { onClick={() => setFilter(tech)}
e.stopPropagation();
setFilter(tech);
}}
className="tech-tag hover:border-primary-custom cursor-pointer" className="tech-tag hover:border-primary-custom cursor-pointer"
title={`Filter by ${tech}`} title={`Filter by ${tech}`}
> >
@@ -218,34 +190,29 @@ export default function Projects({
</div> </div>
</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"> <div className="flex flex-wrap items-center gap-3 pt-5 border-t border-border-custom">
<button <a
type="button" href={`/projects/${flagshipProject.slug}`}
onClick={(e) => { 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"
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"
> >
Inspect Overview View Project
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
</svg> </svg>
</button> </a>
{flagshipProject.repositoryUrl && ( {flagshipProject.repositoryUrl && (
<a <a
href={flagshipProject.repositoryUrl} href={flagshipProject.repositoryUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" 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"> <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"/> <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> </svg>
Source Code
</a> </a>
)} )}
@@ -254,10 +221,9 @@ export default function Projects({
href={flagshipProject.liveUrl} href={flagshipProject.liveUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" className="inline-flex items-center gap-1 text-xs font-mono font-medium text-primary-custom hover:underline ml-auto"
> >
Live Demo ↗ Demo ↗
</a> </a>
)} )}
</div> </div>
@@ -279,14 +245,15 @@ export default function Projects({
return ( return (
<ScrollReveal key={project.slug} variant="fade-up" delay={idx * 80}> <ScrollReveal key={project.slug} variant="fade-up" delay={idx * 80}>
<article <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">
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"
>
<div> <div>
{/* Project Screenshot Viewport for Secondary Cards */} {/* Project Screenshot Viewport for Secondary Cards */}
{project.coverImage ? ( {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 && ( {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"> <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" /> <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" className="w-full h-full object-cover group-hover/submockup:scale-[1.03] transition-transform duration-500 ease-out"
/> />
{/* Hover overlay hint */} {/* 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"> <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"> <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">
View Details ↗ <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> </span>
</div> </div>
</div> </a>
) : null} ) : null}
{/* Header Meta */} {/* Header Meta */}
@@ -327,9 +297,14 @@ export default function Projects({
</div> </div>
{/* Title */} {/* 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"> <a
{project.title} href={`/projects/${project.slug}`}
</h3> 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 */} {/* Summary */}
<p className="text-sm text-muted-foreground-custom leading-relaxed mt-2.5 mb-4 font-normal"> <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) => ( {project.technologies.map((tech) => (
<button <button
key={tech} key={tech}
onClick={(e) => { onClick={() => setFilter(tech)}
e.stopPropagation();
setFilter(tech);
}}
className="tech-tag hover:border-primary-custom cursor-pointer" className="tech-tag hover:border-primary-custom cursor-pointer"
title={`Filter by ${tech}`} title={`Filter by ${tech}`}
> >
@@ -366,21 +338,17 @@ export default function Projects({
))} ))}
</div> </div>
{/* Action Links */} {/* Action Links with basic words */}
<div className="flex items-center justify-between gap-3 pt-3 border-t border-border-custom"> <div className="flex items-center justify-between gap-3 pt-3 border-t border-border-custom">
<button <a
type="button" href={`/projects/${project.slug}`}
onClick={(e) => { className="inline-flex items-center gap-1 text-xs font-semibold text-foreground-custom hover:text-primary-custom transition-colors"
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"
> >
Quick View View Project
<svg className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg> </svg>
</button> </a>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{project.repositoryUrl && ( {project.repositoryUrl && (
@@ -388,9 +356,8 @@ export default function Projects({
href={project.repositoryUrl} href={project.repositoryUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" 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" 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"> <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"/> <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} href={project.liveUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-xs font-mono font-medium text-primary-custom hover:underline" className="text-xs font-mono font-medium text-primary-custom hover:underline"
> >
Demo ↗ Demo ↗
@@ -430,7 +396,7 @@ export default function Projects({
onClick={() => setFilter('All')} onClick={() => setFilter('All')}
className="px-4 py-2 rounded-xl bg-primary-custom text-white text-xs font-semibold cursor-pointer" className="px-4 py-2 rounded-xl bg-primary-custom text-white text-xs font-semibold cursor-pointer"
> >
Reset Filters Reset
</button> </button>
</div> </div>
)} )}
@@ -453,7 +419,7 @@ export default function Projects({
href="/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" 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"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
</svg> </svg>
@@ -463,190 +429,6 @@ export default function Projects({
)} )}
</div> </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> </section>
); );
} }
+283 -138
View File
@@ -6,13 +6,18 @@ import { getProjects, getSiteSettings, getSocialLinks } from '../../lib/data';
const { slug } = Astro.params; const { slug } = Astro.params;
const allProjects = await getProjects(); 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 settings = await getSiteSettings();
const socialLinks = await getSocialLinks(); const socialLinks = await getSocialLinks();
if (!project) { if (!project) {
return Astro.redirect('/404'); 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 <Layout
@@ -22,164 +27,304 @@ if (!project) {
> >
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load /> <Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
<main class="flex-1 pt-6 sm:pt-10 pb-16 relative overflow-hidden"> <main class="flex-1 pt-6 sm:pt-10 pb-20 relative overflow-hidden">
<!-- Background elements --> <!-- Subtle ambient background glows (no enclosing border) -->
<div className="absolute inset-0 z-0 overflow-hidden pointer-events-none"> <div class="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 class="absolute -top-32 left-1/3 w-[600px] h-[500px] rounded-full bg-primary-custom/5 blur-3xl"></div>
<div className="absolute bottom-20 right-1/4 w-80 h-80 rounded-full bg-secondary-custom/5 blur-3xl" /> <div class="absolute top-1/2 right-10 w-96 h-96 rounded-full bg-secondary-custom/5 blur-3xl"></div>
</div> </div>
<div class="container mx-auto px-6 max-w-3xl relative z-10"> <div class="container mx-auto px-4 sm:px-6 lg:px-8 max-w-6xl relative z-10">
<!-- Back to projects -->
<a {/* Top Navigation & Meta Breadcrumb */}
href="/projects" <div class="flex flex-wrap items-center justify-between gap-4 mb-8">
class="inline-flex items-center gap-1.5 text-xs font-bold text-muted-foreground-custom hover:text-primary-custom transition-colors mb-10" <a
> href="/projects"
<svg class="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"> class="inline-flex items-center gap-2 text-xs font-mono font-medium text-muted-foreground-custom hover:text-foreground-custom transition-colors group"
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" /> >
</svg> <svg class="w-4 h-4 group-hover:-translate-x-1 transition-transform" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
Back to projects <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</a> </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"> <div class="flex items-center gap-2.5">
{/* Header banner */} {project.category && (
<div class="absolute top-0 left-0 right-0 h-[3px] bg-gradient-to-r from-primary-custom via-secondary-custom to-primary-custom" /> <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}
<div class="flex items-center justify-between mb-4"> </span>
<span class="text-[10px] uppercase tracking-widest font-bold text-primary-custom bg-primary-custom/10 px-3 py-1 rounded-full"> )}
Project Showcase {project.status && (
</span> <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="text-xs font-semibold text-muted-foreground-custom"> <span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
Published: {project.publishedAt} {project.status}
</span> </span>
)}
</div> </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} {project.title}
</h1> </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} {project.summary}
</p> </p>
{/* Project Cover & Gallery */} {/* Primary Quick Action Buttons */}
{project.coverImage ? ( <div class="flex flex-wrap items-center gap-3">
<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>
)}
{project.liveUrl && ( {project.liveUrl && (
<a <a
href={project.liveUrl} href={project.liveUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" 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 Demo
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"> <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 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" /> <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> </svg>
</a> </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> </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> </div>
</main> </main>