feat: pagina de detalhes do projeto com dados reais, galeria e lightbox
This commit is contained in:
317
frontend/src/app/[locale]/projetos/[id]/page.tsx
Normal file
317
frontend/src/app/[locale]/projetos/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user