feat: pagina de detalhes do projeto com dados reais, galeria e lightbox

This commit is contained in:
Erik
2025-11-27 17:49:51 -03:00
parent a5d42028e6
commit 61d8f707dc
2 changed files with 563 additions and 108 deletions

View File

@@ -0,0 +1,317 @@
"use client";
import Link from "next/link";
import { notFound } from "next/navigation";
import { useState, useEffect } from "react";
import { useLocale } from "@/contexts/LocaleContext";
interface Project {
id: string;
title: string;
category: string;
client: string | null;
status: string;
completionDate: string | null;
description: string | null;
coverImage: string | null;
galleryImages: string[];
featured: boolean;
createdAt: string;
}
export default function ProjectDetails({ params }: { params: { id: string; locale: string } }) {
const { t, locale } = useLocale();
const prefix = locale === 'pt' ? '' : `/${locale}`;
const [project, setProject] = useState<Project | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [lightboxOpen, setLightboxOpen] = useState(false);
// Translations
const texts = {
aboutProject: locale === 'pt' ? 'Sobre o Projeto' : locale === 'es' ? 'Sobre el Proyecto' : 'About the Project',
imageGallery: locale === 'pt' ? 'Galeria de Imagens' : locale === 'es' ? 'Galería de Imágenes' : 'Image Gallery',
technicalSheet: locale === 'pt' ? 'Ficha Técnica' : locale === 'es' ? 'Ficha Técnica' : 'Technical Sheet',
client: locale === 'pt' ? 'Cliente' : locale === 'es' ? 'Cliente' : 'Client',
category: locale === 'pt' ? 'Categoria' : locale === 'es' ? 'Categoría' : 'Category',
status: locale === 'pt' ? 'Status' : locale === 'es' ? 'Estado' : 'Status',
year: locale === 'pt' ? 'Ano' : locale === 'es' ? 'Año' : 'Year',
featured: locale === 'pt' ? 'Destaque' : locale === 'es' ? 'Destacado' : 'Featured',
yes: locale === 'pt' ? 'Sim' : locale === 'es' ? 'Sí' : 'Yes',
requestQuote: locale === 'pt' ? 'Solicitar Orçamento Similar' : locale === 'es' ? 'Solicitar Presupuesto Similar' : 'Request Similar Quote',
backToProjects: locale === 'pt' ? 'Voltar para Projetos' : locale === 'es' ? 'Volver a Proyectos' : 'Back to Projects',
completed: locale === 'pt' ? 'Concluído' : locale === 'es' ? 'Completado' : 'Completed',
inProgress: locale === 'pt' ? 'Em andamento' : locale === 'es' ? 'En progreso' : 'In Progress',
};
useEffect(() => {
async function fetchProject() {
try {
const res = await fetch(`/api/projects/${params.id}`, { cache: "no-store" });
if (res.ok) {
const data = await res.json();
setProject(data);
setSelectedImage(data.coverImage || data.galleryImages?.[0] || null);
} else if (res.status === 404) {
setError(true);
}
} catch (err) {
console.error("Erro ao carregar projeto:", err);
setError(true);
} finally {
setLoading(false);
}
}
fetchProject();
}, [params.id]);
if (loading) {
return (
<main className="bg-white dark:bg-secondary min-h-screen">
<div className="animate-pulse">
<div className="h-[500px] bg-gray-300 dark:bg-gray-700"></div>
<div className="container mx-auto px-4 py-20">
<div className="flex flex-col lg:flex-row gap-16">
<div className="lg:w-2/3 space-y-4">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/3"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-full"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-2/3"></div>
</div>
<div className="lg:w-1/3">
<div className="h-64 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
</div>
</div>
</div>
</div>
</main>
);
}
if (error || !project) {
notFound();
}
const defaultImage = "https://images.unsplash.com/photo-1504307651254-35680f356dfd?q=80&w=2070&auto=format&fit=crop";
const heroImage = project.coverImage || defaultImage;
const allImages = [
...(project.coverImage ? [project.coverImage] : []),
...project.galleryImages,
].filter(Boolean);
const completionYear = project.completionDate
? new Date(project.completionDate).getFullYear()
: new Date(project.createdAt).getFullYear();
const statusText = project.status === 'Concluído' ? texts.completed : texts.inProgress;
return (
<main className="bg-white dark:bg-secondary transition-colors duration-300">
{/* Hero Section */}
<section className="relative h-[500px] flex items-center bg-secondary text-white overflow-hidden">
<div className="absolute inset-0 bg-black/60 z-10"></div>
<div
className="absolute inset-0 bg-cover bg-center"
style={{ backgroundImage: `url('${heroImage}')` }}
></div>
<div className="container mx-auto px-4 relative z-20">
{project.category && (
<span className="inline-block px-3 py-1 bg-primary text-white text-sm font-bold rounded-md mb-4 uppercase tracking-wider">
{project.category}
</span>
)}
<h1 className="text-4xl md:text-6xl font-bold font-headline mb-4 leading-tight max-w-4xl">
{project.title}
</h1>
<div className="flex items-center gap-4 text-gray-300 text-lg flex-wrap">
{project.client && (
<div className="flex items-center gap-2">
<i className="ri-building-line text-primary"></i>
<span>{project.client}</span>
</div>
)}
<div className="flex items-center gap-2">
<i className="ri-calendar-line text-primary"></i>
<span>{completionYear}</span>
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 text-xs rounded ${project.status === 'Concluído' ? 'bg-green-500/20 text-green-300' : 'bg-yellow-500/20 text-yellow-300'}`}>
{statusText}
</span>
</div>
</div>
</div>
</section>
{/* Content Section */}
<section className="py-20 bg-white dark:bg-secondary">
<div className="container mx-auto px-4">
<div className="flex flex-col lg:flex-row gap-16">
{/* Main Content */}
<div className="lg:w-2/3">
{/* Description */}
{project.description && (
<>
<h2 className="text-3xl font-bold font-headline text-secondary dark:text-white mb-6">
{texts.aboutProject}
</h2>
<p className="text-gray-600 dark:text-gray-400 text-lg leading-relaxed mb-12 whitespace-pre-line">
{project.description}
</p>
</>
)}
{/* Image Gallery */}
{allImages.length > 0 && (
<>
<h3 className="text-2xl font-bold font-headline text-secondary dark:text-white mb-6">
{texts.imageGallery}
</h3>
{/* Main Image */}
<div
className="relative aspect-video rounded-xl overflow-hidden mb-4 cursor-pointer group"
onClick={() => {
setLightboxOpen(true);
}}
>
<img
src={selectedImage || heroImage}
alt={project.title}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center">
<i className="ri-zoom-in-line text-4xl text-white opacity-0 group-hover:opacity-100 transition-opacity"></i>
</div>
</div>
{/* Thumbnail Grid */}
{allImages.length > 1 && (
<div className="grid grid-cols-4 md:grid-cols-6 gap-2">
{allImages.map((img, index) => (
<button
key={index}
onClick={() => setSelectedImage(img)}
className={`aspect-square rounded-lg overflow-hidden border-2 transition-all ${
selectedImage === img
? 'border-primary ring-2 ring-primary/30'
: 'border-transparent hover:border-gray-300 dark:hover:border-white/30'
}`}
>
<img
src={img}
alt={`${project.title} - ${index + 1}`}
className="w-full h-full object-cover"
/>
</button>
))}
</div>
)}
</>
)}
</div>
{/* Sidebar */}
<div className="lg:w-1/3">
<div className="bg-gray-50 dark:bg-white/5 p-8 rounded-xl border border-gray-100 dark:border-white/10 sticky top-24">
<h3 className="text-xl font-bold font-headline text-secondary dark:text-white mb-6">
{texts.technicalSheet}
</h3>
<ul className="space-y-4 mb-8">
{project.client && (
<li className="flex justify-between border-b border-gray-200 dark:border-white/10 pb-3">
<span className="text-gray-500 dark:text-gray-400">{texts.client}</span>
<span className="font-medium text-secondary dark:text-white">{project.client}</span>
</li>
)}
{project.category && (
<li className="flex justify-between border-b border-gray-200 dark:border-white/10 pb-3">
<span className="text-gray-500 dark:text-gray-400">{texts.category}</span>
<span className="font-medium text-secondary dark:text-white">{project.category}</span>
</li>
)}
<li className="flex justify-between border-b border-gray-200 dark:border-white/10 pb-3">
<span className="text-gray-500 dark:text-gray-400">{texts.status}</span>
<span className={`font-medium ${project.status === 'Concluído' ? 'text-green-600' : 'text-yellow-600'}`}>
{statusText}
</span>
</li>
<li className="flex justify-between border-b border-gray-200 dark:border-white/10 pb-3">
<span className="text-gray-500 dark:text-gray-400">{texts.year}</span>
<span className="font-medium text-secondary dark:text-white">{completionYear}</span>
</li>
{project.featured && (
<li className="flex justify-between border-b border-gray-200 dark:border-white/10 pb-3">
<span className="text-gray-500 dark:text-gray-400">{texts.featured}</span>
<span className="font-medium text-primary">
<i className="ri-star-fill"></i> {texts.yes}
</span>
</li>
)}
</ul>
<Link href={`${prefix}/contato`} className="block w-full py-4 bg-primary text-white text-center rounded-lg font-bold hover:bg-primary/90 transition-colors">
{texts.requestQuote}
</Link>
<Link href={`${prefix}/projetos`} className="block w-full py-4 mt-4 border border-gray-300 dark:border-white/20 text-gray-600 dark:text-gray-300 text-center rounded-lg font-bold hover:bg-gray-100 dark:hover:bg-white/10 transition-colors">
{texts.backToProjects}
</Link>
</div>
</div>
</div>
</div>
</section>
{/* Lightbox */}
{lightboxOpen && (
<div
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4"
onClick={() => setLightboxOpen(false)}
>
<button
className="absolute top-4 right-4 text-white text-4xl hover:text-primary transition-colors"
onClick={() => setLightboxOpen(false)}
>
<i className="ri-close-line"></i>
</button>
{allImages.length > 1 && (
<>
<button
className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl hover:text-primary transition-colors"
onClick={(e) => {
e.stopPropagation();
const currentIndex = allImages.indexOf(selectedImage || '');
const prevIndex = currentIndex <= 0 ? allImages.length - 1 : currentIndex - 1;
setSelectedImage(allImages[prevIndex]);
}}
>
<i className="ri-arrow-left-s-line"></i>
</button>
<button
className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl hover:text-primary transition-colors"
onClick={(e) => {
e.stopPropagation();
const currentIndex = allImages.indexOf(selectedImage || '');
const nextIndex = currentIndex >= allImages.length - 1 ? 0 : currentIndex + 1;
setSelectedImage(allImages[nextIndex]);
}}
>
<i className="ri-arrow-right-s-line"></i>
</button>
</>
)}
<img
src={selectedImage || heroImage}
alt={project.title}
className="max-w-full max-h-[90vh] object-contain"
onClick={(e) => e.stopPropagation()}
/>
</div>
)}
</main>
);
}