feat: enable project catalog management
This commit is contained in:
@@ -1,63 +1,79 @@
|
||||
"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;
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// Placeholder data - will be replaced by database content
|
||||
const projects = [
|
||||
{
|
||||
id: 1,
|
||||
title: t('projects.items.item1.title'),
|
||||
category: t('projects.categories.vehicular'),
|
||||
location: "Vitória, ES",
|
||||
image: "https://images.unsplash.com/photo-1616401784845-180882ba9ba8?q=80&w=2070&auto=format&fit=crop",
|
||||
description: t('projects.items.item1.description')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: t('projects.items.item2.title'),
|
||||
category: t('projects.categories.reports'),
|
||||
location: "Serra, ES",
|
||||
image: "https://images.unsplash.com/photo-1535082623926-b3a33d531740?q=80&w=2052&auto=format&fit=crop",
|
||||
description: t('projects.items.item2.description')
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: t('projects.items.item3.title'),
|
||||
category: t('projects.categories.mechanical'),
|
||||
location: "Aracruz, ES",
|
||||
image: "https://images.unsplash.com/photo-1504917595217-d4dc5ebe6122?q=80&w=2070&auto=format&fit=crop",
|
||||
description: t('projects.items.item3.description')
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: t('projects.items.item4.title'),
|
||||
category: t('projects.categories.safety'),
|
||||
location: "Linhares, ES",
|
||||
image: "https://images.unsplash.com/photo-1581092921461-eab62e97a782?q=80&w=2070&auto=format&fit=crop",
|
||||
description: t('projects.items.item4.description')
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: t('projects.items.item5.title'),
|
||||
category: t('projects.categories.vehicular'),
|
||||
location: "Viana, ES",
|
||||
image: "https://images.unsplash.com/photo-1591768793355-74d04bb6608f?q=80&w=2070&auto=format&fit=crop",
|
||||
description: t('projects.items.item5.description')
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: t('projects.items.item6.title'),
|
||||
category: t('projects.categories.safety'),
|
||||
location: "Cariacica, ES",
|
||||
image: "https://images.unsplash.com/photo-1504328345606-18bbc8c9d7d1?q=80&w=2070&auto=format&fit=crop",
|
||||
description: t('projects.items.item6.description')
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const controller = new AbortController();
|
||||
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/projects?status=published', { signal: controller.signal });
|
||||
if (!response.ok) {
|
||||
throw new Error('Falha ao buscar projetos');
|
||||
}
|
||||
const data: Project[] = await response.json();
|
||||
if (isMounted) {
|
||||
setProjects(data);
|
||||
}
|
||||
} 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(() => {
|
||||
if (selectedCategory === 'todos') {
|
||||
return projects;
|
||||
}
|
||||
];
|
||||
return projects.filter((project) => project.category === selectedCategory);
|
||||
}, [projects, selectedCategory]);
|
||||
|
||||
return (
|
||||
<main className="bg-white dark:bg-secondary transition-colors duration-300">
|
||||
@@ -78,38 +94,67 @@ export default function ProjetosPage() {
|
||||
<div className="container mx-auto px-4">
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap gap-4 mb-12 justify-center">
|
||||
<button className="px-6 py-2 bg-primary text-white rounded-full font-bold shadow-md">{t('projects.filters.all')}</button>
|
||||
<button className="px-6 py-2 bg-gray-100 dark:bg-white/10 text-gray-600 dark:text-gray-300 rounded-full font-bold hover:bg-gray-200 dark:hover:bg-white/20 transition-colors">{t('projects.filters.implements')}</button>
|
||||
<button className="px-6 py-2 bg-gray-100 dark:bg-white/10 text-gray-600 dark:text-gray-300 rounded-full font-bold hover:bg-gray-200 dark:hover:bg-white/20 transition-colors">{t('projects.filters.mechanical')}</button>
|
||||
<button className="px-6 py-2 bg-gray-100 dark:bg-white/10 text-gray-600 dark:text-gray-300 rounded-full font-bold hover:bg-gray-200 dark:hover:bg-white/20 transition-colors">{t('projects.filters.reports')}</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{projects.map((project) => (
|
||||
<div key={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">
|
||||
<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('${project.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}
|
||||
</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-map-pin-line"></i>
|
||||
<span>{project.location}</span>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm mb-6 line-clamp-3 grow">
|
||||
{project.description}
|
||||
</p>
|
||||
<Link href={`${prefix}/projetos/${project.id}`} className="inline-flex items-center gap-2 text-primary font-bold hover:gap-3 transition-all mt-auto">
|
||||
{t('projects.viewDetails')} <i className="ri-arrow-right-line"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<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-briefcase-line text-5xl mb-4"></i>
|
||||
<p>{locale === 'pt' ? 'Ainda não temos projetos publicados nesta categoria.' : locale === 'es' ? 'Todavía no hay proyectos publicados en esta categoría.' : 'No projects published in this category yet.'}</p>
|
||||
</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 (
|
||||
<div key={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">
|
||||
<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>
|
||||
<Link href={`${prefix}/projetos/${project.id}`} className="inline-flex items-center gap-2 text-primary font-bold hover:gap-3 transition-all mt-auto">
|
||||
{t('projects.viewDetails')} <i className="ri-arrow-right-line"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user