feat: pagina de detalhes do projeto com dados reais, galeria e lightbox
This commit is contained in:
@@ -1,157 +1,295 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { T } from "@/components/TranslatedText";
|
||||||
|
|
||||||
// Mock data - same as in the main projects page
|
interface Project {
|
||||||
// In a real app, this would come from a database or API
|
id: string;
|
||||||
const projects = [
|
title: string;
|
||||||
{
|
category: string;
|
||||||
id: 1,
|
client: string | null;
|
||||||
title: "Engenharia de Adequação - Frota Coca-Cola",
|
status: string;
|
||||||
category: "Engenharia Veicular",
|
completionDate: string | null;
|
||||||
location: "Vitória, ES",
|
description: string | null;
|
||||||
image: "https://images.unsplash.com/photo-1616401784845-180882ba9ba8?q=80&w=2070&auto=format&fit=crop",
|
coverImage: string | null;
|
||||||
description: "Projeto de adequação técnica de 50 caminhões para instalação de carrocerias especiais e sistemas de segurança.",
|
galleryImages: string[];
|
||||||
details: "Desenvolvimento completo do projeto de engenharia para adequação de frota de distribuição de bebidas. O escopo incluiu o cálculo estrutural para rebaixamento de carrocerias, instalação de sistemas de proteção lateral e traseira conforme resoluções do CONTRAN, e homologação junto aos órgãos competentes. O projeto resultou em aumento de 15% na capacidade de carga e total conformidade normativa.",
|
featured: boolean;
|
||||||
features: ["Cálculo Estrutural", "Homologação DENATRAN", "Segurança Operacional", "Adequação de Carroceria"]
|
createdAt: string;
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "Laudo de Guindaste Articulado",
|
|
||||||
category: "Inspeção Técnica",
|
|
||||||
location: "Serra, ES",
|
|
||||||
image: "https://images.unsplash.com/photo-1535082623926-b3a33d531740?q=80&w=2052&auto=format&fit=crop",
|
|
||||||
description: "Inspeção completa e emissão de laudo técnico para guindaste de 45 toneladas, com testes de carga e verificação estrutural.",
|
|
||||||
details: "Realização de inspeção detalhada em guindaste articulado (Munck) com capacidade de 45 toneladas. Foram realizados ensaios não destrutivos (líquido penetrante) em pontos críticos de solda, verificação do sistema hidráulico, testes de carga estática e dinâmica conforme NR-11. O laudo técnico atestou a integridade do equipamento para operação segura.",
|
|
||||||
features: ["Ensaio Não Destrutivo", "Teste de Carga", "Verificação Hidráulica", "ART de Inspeção"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "Projeto de Dispositivo de Içamento",
|
|
||||||
category: "Projeto Mecânico",
|
|
||||||
location: "Aracruz, ES",
|
|
||||||
image: "https://images.unsplash.com/photo-1504917595217-d4dc5ebe6122?q=80&w=2070&auto=format&fit=crop",
|
|
||||||
description: "Desenvolvimento e cálculo estrutural de Spreader para movimentação de contêineres em área portuária.",
|
|
||||||
details: "Projeto mecânico de um Spreader (balancim) automático para içamento de contêineres de 20 e 40 pés. O dispositivo foi projetado para suportar cargas de até 30 toneladas, com sistema de travamento twist-lock automático. Entregamos o projeto completo em 3D, desenhos de fabricação, memorial de cálculo e manual de operação.",
|
|
||||||
features: ["Modelagem 3D", "Cálculo de Elementos Finitos", "Detalhamento de Fabricação", "Manual de Operação"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "Certificação NR-12 - Parque Industrial",
|
|
||||||
category: "Laudos",
|
|
||||||
location: "Linhares, ES",
|
|
||||||
image: "https://images.unsplash.com/photo-1581092921461-eab62e97a782?q=80&w=2070&auto=format&fit=crop",
|
|
||||||
description: "Inventário e adequação de segurança de 120 máquinas operatrizes conforme norma regulamentadora NR-12.",
|
|
||||||
details: "Consultoria completa para adequação à NR-12 em parque fabril. Realizamos o inventário de 120 máquinas, análise de risco (HRN), projeto de proteções mecânicas e sistemas de segurança eletrônica. Acompanhamos a implementação e emitimos os laudos de validação final, garantindo a segurança dos operadores.",
|
|
||||||
features: ["Análise de Risco", "Projeto de Proteções", "Sistemas de Segurança", "Laudo de Validação"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: "Homologação de Plataforma Elevatória",
|
|
||||||
category: "Engenharia Veicular",
|
|
||||||
location: "Viana, ES",
|
|
||||||
image: "https://images.unsplash.com/photo-1591768793355-74d04bb6608f?q=80&w=2070&auto=format&fit=crop",
|
|
||||||
description: "Processo completo de homologação e certificação de plataformas elevatórias para distribuição urbana.",
|
|
||||||
details: "Assessoria técnica para fabricante de plataformas elevatórias veiculares. Realizamos os cálculos de estabilidade, testes de tombamento e resistência estrutural necessários para a obtenção do CAT (Certificado de Adequação à Legislação de Trânsito). O equipamento foi homologado com sucesso para uso em veículos urbanos de carga.",
|
|
||||||
features: ["Cálculo de Estabilidade", "Teste de Tombamento", "Dossiê Técnico", "Homologação INMETRO/DENATRAN"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: "Projeto de Linha de Vida para Caminhões",
|
|
||||||
category: "Segurança do Trabalho",
|
|
||||||
location: "Cariacica, ES",
|
|
||||||
image: "https://images.unsplash.com/photo-1504328345606-18bbc8c9d7d1?q=80&w=2070&auto=format&fit=crop",
|
|
||||||
description: "Projeto e instalação de sistema de linha de vida para proteção contra quedas em operações de carga e descarga.",
|
|
||||||
details: "Projeto e instalação de sistema de linha de vida rígida sobre estrutura metálica para proteção de quedas durante o enlonamento de caminhões. O sistema permite que o operador trabalhe com segurança em toda a extensão da carroceria. Fornecimento de projeto, ART e treinamento de uso para a equipe.",
|
|
||||||
features: ["Projeto Estrutural", "Sistema de Ancoragem", "Treinamento NR-35", "ART de Instalação"]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ProjectDetails({ params }: { params: { id: string } }) {
|
export default function ProjectDetails({ params }: { params: { id: string } }) {
|
||||||
const project = projects.find((p) => p.id === parseInt(params.id));
|
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);
|
||||||
|
|
||||||
if (!project) {
|
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();
|
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();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main className="bg-white dark:bg-secondary transition-colors duration-300">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="relative h-[500px] flex items-center bg-secondary text-white overflow-hidden">
|
<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-black/60 z-10"></div>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-cover bg-center"
|
className="absolute inset-0 bg-cover bg-center"
|
||||||
style={{ backgroundImage: `url('${project.image}')` }}
|
style={{ backgroundImage: `url('${heroImage}')` }}
|
||||||
></div>
|
></div>
|
||||||
<div className="container mx-auto px-4 relative z-20">
|
<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">
|
<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}
|
{project.category}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
<h1 className="text-4xl md:text-6xl font-bold font-headline mb-4 leading-tight max-w-4xl">
|
<h1 className="text-4xl md:text-6xl font-bold font-headline mb-4 leading-tight max-w-4xl">
|
||||||
{project.title}
|
{project.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-center gap-2 text-gray-300 text-lg">
|
<div className="flex items-center gap-4 text-gray-300 text-lg flex-wrap">
|
||||||
<i className="ri-map-pin-line text-primary"></i>
|
{project.client && (
|
||||||
<span>{project.location}</span>
|
<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'}`}>
|
||||||
|
{project.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Content Section */}
|
{/* Content Section */}
|
||||||
<section className="py-20 bg-white">
|
<section className="py-20 bg-white dark:bg-secondary">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
<div className="flex flex-col lg:flex-row gap-16">
|
<div className="flex flex-col lg:flex-row gap-16">
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="lg:w-2/3">
|
<div className="lg:w-2/3">
|
||||||
<h2 className="text-3xl font-bold font-headline text-secondary mb-6">Sobre o Projeto</h2>
|
{/* Description */}
|
||||||
<p className="text-gray-600 text-lg leading-relaxed mb-8">
|
{project.description && (
|
||||||
{project.details}
|
<>
|
||||||
|
<h2 className="text-3xl font-bold font-headline text-secondary dark:text-white mb-6">
|
||||||
|
<T>Sobre o Projeto</T>
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-lg leading-relaxed mb-12 whitespace-pre-line">
|
||||||
|
{project.description}
|
||||||
</p>
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<h3 className="text-2xl font-bold font-headline text-secondary mb-6">Escopo Técnico</h3>
|
{/* Image Gallery */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
{allImages.length > 0 && (
|
||||||
{project.features.map((feature, index) => (
|
<>
|
||||||
<div key={index} className="flex items-center gap-3 p-4 bg-gray-50 rounded-lg border border-gray-100">
|
<h3 className="text-2xl font-bold font-headline text-secondary dark:text-white mb-6">
|
||||||
<i className="ri-checkbox-circle-line text-primary text-xl"></i>
|
<T>Galeria de Imagens</T>
|
||||||
<span className="font-medium text-gray-700">{feature}</span>
|
</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>
|
||||||
|
</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} - Imagem ${index + 1}`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<div className="lg:w-1/3">
|
<div className="lg:w-1/3">
|
||||||
<div className="bg-gray-50 p-8 rounded-xl border border-gray-100 sticky top-24">
|
<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 mb-6">Ficha Técnica</h3>
|
<h3 className="text-xl font-bold font-headline text-secondary dark:text-white mb-6">
|
||||||
|
<T>Ficha Técnica</T>
|
||||||
|
</h3>
|
||||||
<ul className="space-y-4 mb-8">
|
<ul className="space-y-4 mb-8">
|
||||||
<li className="flex justify-between border-b border-gray-200 pb-3">
|
{project.client && (
|
||||||
<span className="text-gray-500">Cliente</span>
|
<li className="flex justify-between border-b border-gray-200 dark:border-white/10 pb-3">
|
||||||
<span className="font-medium text-secondary">Confidencial</span>
|
<span className="text-gray-500 dark:text-gray-400"><T>Cliente</T></span>
|
||||||
|
<span className="font-medium text-secondary dark:text-white">{project.client}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex justify-between border-b border-gray-200 pb-3">
|
)}
|
||||||
<span className="text-gray-500">Categoria</span>
|
{project.category && (
|
||||||
<span className="font-medium text-secondary">{project.category}</span>
|
<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"><T>Categoria</T></span>
|
||||||
|
<span className="font-medium text-secondary dark:text-white">{project.category}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex justify-between border-b border-gray-200 pb-3">
|
)}
|
||||||
<span className="text-gray-500">Local</span>
|
<li className="flex justify-between border-b border-gray-200 dark:border-white/10 pb-3">
|
||||||
<span className="font-medium text-secondary">{project.location}</span>
|
<span className="text-gray-500 dark:text-gray-400"><T>Status</T></span>
|
||||||
|
<span className={`font-medium ${project.status === 'Concluído' ? 'text-green-600' : 'text-yellow-600'}`}>
|
||||||
|
{project.status}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex justify-between border-b border-gray-200 pb-3">
|
<li className="flex justify-between border-b border-gray-200 dark:border-white/10 pb-3">
|
||||||
<span className="text-gray-500">Ano</span>
|
<span className="text-gray-500 dark:text-gray-400"><T>Ano</T></span>
|
||||||
<span className="font-medium text-secondary">2024</span>
|
<span className="font-medium text-secondary dark:text-white">{completionYear}</span>
|
||||||
</li>
|
</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"><T>Destaque</T></span>
|
||||||
|
<span className="font-medium text-primary">
|
||||||
|
<i className="ri-star-fill"></i> <T>Sim</T>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Link href="/contato" className="block w-full py-4 bg-primary text-white text-center rounded-lg font-bold hover-primary transition-colors">
|
<Link href="/contato" className="block w-full py-4 bg-primary text-white text-center rounded-lg font-bold hover:bg-primary/90 transition-colors">
|
||||||
Solicitar Orçamento Similar
|
<T>Solicitar Orçamento Similar</T>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/projetos" className="block w-full py-4 mt-4 border border-gray-300 text-gray-600 text-center rounded-lg font-bold hover:bg-gray-100 transition-colors">
|
<Link href="/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">
|
||||||
Voltar para Projetos
|
<T>Voltar para Projetos</T>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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