From 89b5a2edc1dd9da3416ee2dc0f89e30932773581 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 27 Nov 2025 18:16:31 -0300 Subject: [PATCH] feat: adicionar botao e pagina de edicao de projetos no admin --- .../app/admin/projetos/[id]/editar/page.tsx | 479 ++++++++++++++++++ frontend/src/app/admin/projetos/page.tsx | 7 + 2 files changed, 486 insertions(+) create mode 100644 frontend/src/app/admin/projetos/[id]/editar/page.tsx diff --git a/frontend/src/app/admin/projetos/[id]/editar/page.tsx b/frontend/src/app/admin/projetos/[id]/editar/page.tsx new file mode 100644 index 0000000..6c2eee5 --- /dev/null +++ b/frontend/src/app/admin/projetos/[id]/editar/page.tsx @@ -0,0 +1,479 @@ +"use client"; + +import { useRef, useState, useEffect } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useToast } from '@/contexts/ToastContext'; + +type UploadedImage = { + url: string; + path: string; + name: string; +}; + +const CATEGORY_OPTIONS = [ + { value: 'Engenharia Veicular', label: 'Engenharia Veicular' }, + { value: 'Projetos Mecânicos', label: 'Projetos Mecânicos' }, + { value: 'Laudos e Inspeções', label: 'Laudos e Inspeções' }, + { value: 'Segurança do Trabalho', label: 'Segurança do Trabalho' }, +]; + +const STATUS_OPTIONS = [ + { value: 'Concluído', label: 'Concluído' }, + { value: 'Em andamento', label: 'Em andamento' }, + { value: 'Rascunho', label: 'Rascunho' }, +]; + +export default function EditProject({ params }: { params: { id: string } }) { + const router = useRouter(); + const { success, error } = useToast(); + const coverInputRef = useRef(null); + const galleryInputRef = useRef(null); + const [loadingData, setLoadingData] = useState(true); + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + title: '', + category: '', + client: '', + status: 'Concluído', + description: '', + date: '', + featured: false, + }); + const [coverImage, setCoverImage] = useState(null); + const [galleryImages, setGalleryImages] = useState([]); + const [uploadingCover, setUploadingCover] = useState(false); + const [uploadingGallery, setUploadingGallery] = useState(false); + + const isSaving = loading || uploadingCover || uploadingGallery; + + // Carregar dados do projeto + useEffect(() => { + async function fetchProject() { + try { + const res = await fetch(`/api/projects/${params.id}`); + if (!res.ok) { + throw new Error('Projeto não encontrado'); + } + const project = await res.json(); + + setFormData({ + title: project.title || '', + category: project.category || '', + client: project.client || '', + status: project.status || 'Concluído', + description: project.description || '', + date: project.completionDate ? project.completionDate.split('T')[0] : '', + featured: project.featured || false, + }); + + if (project.coverImage) { + setCoverImage({ + url: project.coverImage, + path: project.coverImage, + name: 'Imagem de capa', + }); + } + + if (project.galleryImages && project.galleryImages.length > 0) { + setGalleryImages( + project.galleryImages.map((url: string, index: number) => ({ + url, + path: url, + name: `Imagem ${index + 1}`, + })) + ); + } + } catch (err) { + console.error('Erro ao carregar projeto:', err); + error('Não foi possível carregar o projeto.'); + router.push('/admin/projetos'); + } finally { + setLoadingData(false); + } + } + fetchProject(); + }, [params.id, error, router]); + + const uploadFile = async (file: File): Promise => { + if (file.size > 2 * 1024 * 1024) { + error('Arquivo maior que 2MB. Escolha uma imagem menor.'); + return null; + } + + const supportedTypes = ['image/png', 'image/jpeg', 'image/webp']; + if (!supportedTypes.includes(file.type)) { + error('Formato inválido. Utilize PNG, JPG ou WEBP.'); + return null; + } + + const body = new FormData(); + body.append('file', file); + + const response = await fetch('/api/upload', { + method: 'POST', + body, + }); + + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data?.error || 'Erro ao enviar imagem'); + } + + const data = await response.json(); + return { + url: data.url, + path: data.path, + name: file.name, + }; + }; + + const handleCoverSelect = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + setUploadingCover(true); + try { + const uploaded = await uploadFile(file); + if (uploaded) { + setCoverImage(uploaded); + success('Imagem de capa carregada.'); + } + } catch (err) { + console.error('Erro ao enviar imagem de capa:', err); + error(err instanceof Error ? err.message : 'Falha ao enviar imagem de capa.'); + } finally { + setUploadingCover(false); + event.target.value = ''; + } + }; + + const handleGallerySelect = async (event: React.ChangeEvent) => { + const files = Array.from(event.target.files ?? []); + if (files.length === 0) return; + + if (galleryImages.length + files.length > 8) { + error('Limite de 8 imagens atingido. Remova algumas antes de adicionar novas.'); + event.target.value = ''; + return; + } + + setUploadingGallery(true); + try { + for (const file of files) { + const uploaded = await uploadFile(file); + if (uploaded) { + setGalleryImages((prev) => [...prev, uploaded]); + } + } + success('Galeria atualizada.'); + } catch (err) { + console.error('Erro ao enviar imagem da galeria:', err); + error(err instanceof Error ? err.message : 'Falha ao enviar imagem da galeria.'); + } finally { + setUploadingGallery(false); + event.target.value = ''; + } + }; + + const handleRemoveGalleryImage = (path: string) => { + setGalleryImages((prev) => prev.filter((image) => image.path !== path)); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.title || !formData.category) { + error('Informe ao menos o título e a categoria do projeto.'); + return; + } + + setLoading(true); + try { + const response = await fetch(`/api/projects/${params.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: formData.title.trim(), + category: formData.category, + client: formData.client?.trim() || null, + status: formData.status, + description: formData.description?.trim() || null, + completionDate: formData.date ? new Date(formData.date).toISOString() : null, + coverImage: coverImage?.url ?? null, + galleryImages: galleryImages.map((image) => image.url), + featured: formData.featured, + }), + }); + + const data = await response.json().catch(() => ({})); + + if (!response.ok) { + throw new Error(data?.error || 'Erro ao atualizar projeto'); + } + + success('Projeto atualizado com sucesso!'); + router.push('/admin/projetos'); + } catch (err) { + console.error('Erro ao atualizar projeto:', err); + error(err instanceof Error ? err.message : 'Não foi possível atualizar o projeto.'); + } finally { + setLoading(false); + } + }; + + if (loadingData) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+
+ + + +
+

Editar Projeto

+

Atualize as informações do projeto.

+
+
+ +
+ {/* Basic Info */} +
+

+ + Informações Básicas +

+ +
+
+ + setFormData({...formData, title: e.target.value})} + className="w-full px-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" + placeholder="Ex: Adequação de Frota Coca-Cola" + required + /> +
+ +
+ + +
+ +
+ + setFormData({...formData, client: e.target.value})} + className="w-full px-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" + placeholder="Ex: Coca-Cola FEMSA" + /> +
+ +
+ + setFormData({...formData, date: e.target.value})} + className="w-full px-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" + /> +
+ +
+ + +
+ +
+ + +
+ +
+ +

Projetos em destaque podem ser exibidos em seções especiais como a home.

+
+
+
+ + {/* Media */} +
+

+ + Mídia +

+ +
+
+ +
coverInputRef.current?.click()} + className="border-2 border-dashed border-gray-300 dark:border-white/20 rounded-xl p-6 text-center hover:border-primary dark:hover:border-primary transition-colors cursor-pointer bg-gray-50 dark:bg-white/5 relative min-h-[200px] flex items-center justify-center" + > + {coverImage ? ( +
+ {coverImage.name} + +
+ ) : ( +
+
+ {uploadingCover ? ( + + ) : ( + + )} +
+

+ {uploadingCover ? 'Enviando imagem...' : 'Clique para fazer upload ou arraste e solte'} +

+

PNG, JPG ou WEBP (máximo 2MB)

+
+ )} + +
+
+ +
+ +
+ + + {galleryImages.map((image) => ( +
+ {image.name} +
+ +
+
+ ))} + +
+

Adicione até 8 imagens para apresentar detalhes do projeto.

+
+
+
+ + {/* Actions */} +
+ + Cancelar + + +
+
+
+ ); +} diff --git a/frontend/src/app/admin/projetos/page.tsx b/frontend/src/app/admin/projetos/page.tsx index 4402dcc..fd3b2d4 100644 --- a/frontend/src/app/admin/projetos/page.tsx +++ b/frontend/src/app/admin/projetos/page.tsx @@ -358,6 +358,13 @@ export default function ProjectsList() { {renderStatus(project.status)}
+ + +