Files
octto-engenharia/frontend/src/app/[locale]/projetos/page.tsx

236 lines
11 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useLocale } from "@/contexts/LocaleContext";
interface Project {
id: string;
title: string;
category: string;
description: string | null;
coverImage: string | null;
galleryImages: string[];
status: string;
client: string | null;
completionDate: string | null;
createdAt?: string;
}
const FALLBACK_IMAGE = "https://images.unsplash.com/photo-1616401784845-180882ba9ba8?q=80&w=2070&auto=format&fit=crop";
export default function ProjetosPage() {
const { t, locale } = useLocale();
const prefix = locale === 'pt' ? '' : `/${locale}`;
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const [selectedCategory, setSelectedCategory] = useState<string>('todos');
const [searchTerm, setSearchTerm] = useState("");
const searchPlaceholder = locale === 'pt' ? 'Pesquisar projetos...' : locale === 'es' ? 'Buscar proyectos...' : 'Search projects...';
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const fetchProjects = async () => {
try {
const response = await fetch('/api/projects', {
method: 'GET',
cache: 'no-store',
credentials: 'same-origin',
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`Falha ao buscar projetos (status ${response.status})`);
}
const data = await response.json();
if (isMounted && Array.isArray(data)) {
const publishedProjects = (data as Project[])
.filter((project) => project.status !== 'Rascunho')
.sort((a, b) => {
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
return dateB - dateA;
});
setProjects(publishedProjects);
}
} catch (err) {
if ((err as Error).name !== 'AbortError') {
console.error('Erro ao carregar projetos:', err);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchProjects();
return () => {
isMounted = false;
controller.abort();
};
}, []);
const categories = useMemo(() => {
const unique = new Set<string>();
projects.forEach((project) => {
if (project.category) {
unique.add(project.category);
}
});
return Array.from(unique);
}, [projects]);
const filteredProjects = useMemo(() => {
return projects.filter((project) => {
const matchesCategory = selectedCategory === 'todos' || project.category === selectedCategory;
const matchesSearch =
!searchTerm ||
project.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.category?.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.client?.toLowerCase().includes(searchTerm.toLowerCase());
return matchesCategory && matchesSearch;
});
}, [projects, selectedCategory, searchTerm]);
return (
<main className="bg-white dark:bg-secondary transition-colors duration-300">
{/* Hero Section */}
<section className="relative h-[400px] 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-[url('https://images.unsplash.com/photo-1504307651254-35680f356dfd?q=80&w=2070&auto=format&fit=crop')] bg-cover bg-center"></div>
<div className="container mx-auto px-4 relative z-20">
<h1 className="text-5xl font-bold font-headline mb-4">{t('projects.hero.title')}</h1>
<p className="text-xl text-gray-300 max-w-2xl">
{t('projects.hero.subtitle')}
</p>
</div>
</section>
{/* Projects Grid */}
<section className="py-20 bg-white dark:bg-secondary">
<div className="container mx-auto px-4">
{/* Search Bar */}
<div className="max-w-xl mx-auto mb-8">
<div className="relative">
<input
type="text"
placeholder={searchPlaceholder}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-5 py-3 pl-12 rounded-full border border-gray-200 dark:border-white/20 bg-white dark:bg-white/5 text-secondary dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
/>
<i className="ri-search-line absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 text-xl"></i>
{searchTerm && (
<button
onClick={() => setSearchTerm("")}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-white transition-colors"
>
<i className="ri-close-line text-xl"></i>
</button>
)}
</div>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-4 mb-12 justify-center">
<button
onClick={() => setSelectedCategory('todos')}
className={`px-6 py-2 rounded-full font-bold transition-colors ${selectedCategory === 'todos' ? 'bg-primary text-white shadow-md' : 'bg-gray-100 dark:bg-white/10 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-white/20'}`}
>
{t('projects.filters.all')}
</button>
{categories.map((category) => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-6 py-2 rounded-full font-bold transition-colors ${selectedCategory === category ? 'bg-primary text-white shadow-md' : 'bg-gray-100 dark:bg-white/10 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-white/20'}`}
>
{category}
</button>
))}
</div>
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{Array.from({ length: 6 }).map((_, index) => (
<div key={index} className="animate-pulse bg-white dark:bg-secondary rounded-xl border border-gray-100 dark:border-white/10 h-96"></div>
))}
</div>
) : filteredProjects.length === 0 ? (
<div className="text-center text-gray-500 dark:text-gray-400 py-16">
<i className="ri-folder-open-line text-5xl mb-4"></i>
<h3 className="text-xl font-bold mb-2">
{locale === 'pt' ? 'Nenhum projeto encontrado' : locale === 'es' ? 'Ningún proyecto encontrado' : 'No projects found'}
</h3>
<p>
{searchTerm || selectedCategory !== 'todos'
? (locale === 'pt' ? 'Tente ajustar os filtros de busca' : locale === 'es' ? 'Intente ajustar los filtros de búsqueda' : 'Try adjusting your search filters')
: (locale === 'pt' ? 'Em breve adicionaremos novos projetos' : locale === 'es' ? 'Pronto agregaremos nuevos proyectos' : 'New projects coming soon')}
</p>
{(searchTerm || selectedCategory !== 'todos') && (
<button
onClick={() => {
setSearchTerm("");
setSelectedCategory('todos');
}}
className="mt-4 px-6 py-2 bg-primary text-white rounded-full font-bold hover:bg-primary/90 transition-colors"
>
{locale === 'pt' ? 'Limpar filtros' : locale === 'es' ? 'Limpiar filtros' : 'Clear filters'}
</button>
)}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredProjects.map((project) => {
const image = project.coverImage || project.galleryImages[0] || FALLBACK_IMAGE;
const description = project.description || (locale === 'pt' ? 'Descrição disponível em breve.' : locale === 'es' ? 'Descripción disponible pronto.' : 'Description coming soon.');
return (
<Link key={project.id} href={`${prefix}/projetos/${project.id}`} className="group bg-white dark:bg-secondary rounded-xl overflow-hidden shadow-sm hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-white/10 flex flex-col cursor-pointer">
<div className="relative h-64 overflow-hidden">
<div className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-110" style={{ backgroundImage: `url('${image}')` }}></div>
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/0 transition-colors"></div>
<div className="absolute top-4 left-4 bg-white/90 backdrop-blur-sm px-3 py-1 rounded-md text-xs font-bold text-secondary uppercase tracking-wider">
{project.category || t('projects.filters.all')}
</div>
</div>
<div className="p-6 grow flex flex-col">
<h3 className="text-xl font-bold font-headline text-secondary dark:text-white mb-2 group-hover:text-primary transition-colors">{project.title}</h3>
<div className="flex items-center gap-2 text-gray-500 dark:text-gray-400 text-sm mb-4">
<i className="ri-roadster-line"></i>
<span>{project.client || (locale === 'pt' ? 'Cliente confidencial' : locale === 'es' ? 'Cliente confidencial' : 'Confidential client')}</span>
</div>
<p className="text-gray-600 dark:text-gray-400 text-sm mb-6 line-clamp-3 grow">
{description}
</p>
<span className="inline-flex items-center gap-2 text-primary font-bold group-hover:gap-3 transition-all mt-auto">
{t('projects.viewDetails')} <i className="ri-arrow-right-line"></i>
</span>
</div>
</Link>
);
})}
</div>
)}
{/* Results Count */}
{!loading && filteredProjects.length > 0 && (
<div className="text-center mt-12 text-gray-500 dark:text-gray-400">
{locale === 'pt' ? 'Exibindo' : locale === 'es' ? 'Mostrando' : 'Showing'} {filteredProjects.length} {locale === 'pt' ? 'de' : locale === 'es' ? 'de' : 'of'} {projects.length} {locale === 'pt' ? 'projetos' : locale === 'es' ? 'proyectos' : 'projects'}
</div>
)}
</div>
</section>
</main>
);
}