diff --git a/package-lock.json b/package-lock.json index df0090c..b9d8a52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "lucide-react": "^0.542.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-hot-toast": "^2.6.0", "react-icons": "^5.5.0", "react-router-dom": "^7.8.2", "tailwindcss": "^4.1.12" @@ -2772,7 +2773,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -3318,6 +3318,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4123,6 +4132,23 @@ "react": "^19.1.1" } }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", diff --git a/package.json b/package.json index 8e78519..df1ebda 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "lucide-react": "^0.542.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-hot-toast": "^2.6.0", "react-icons": "^5.5.0", "react-router-dom": "^7.8.2", "tailwindcss": "^4.1.12" diff --git a/src/App.tsx b/src/App.tsx index 4daebb5..949056a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,8 @@ import AdoptionRegister from "./pages/AdoptionRegister.tsx"; import Login from "./pages/Login.tsx"; import PrivateRoute from "./routes/PrivateRoute"; import { AuthProvider } from "./context/AuthContext"; -import type {JSX} from "react"; +import { Toaster } from "react-hot-toast"; // <-- import do Toaster +import type { JSX } from "react"; const ProtectedLayout = ({ children }: { children: JSX.Element }) => (
@@ -21,6 +22,8 @@ const ProtectedLayout = ({ children }: { children: JSX.Element }) => ( export default function App() { return ( + + } /> diff --git a/src/pages/AnimalList.tsx b/src/pages/AnimalList.tsx index a3e893d..f43bd4b 100644 --- a/src/pages/AnimalList.tsx +++ b/src/pages/AnimalList.tsx @@ -1,11 +1,13 @@ import { useState, useEffect } from "react"; import { Plus, X, Trash2 } from "lucide-react"; import {useNavigate} from "react-router-dom"; +import { getAnimals, deleteAnimalById } from "../services/animalService"; +import toast from "react-hot-toast"; -type Animal = { - id: number; +export type Animal = { + id: string; name: string; - age: string; + birthDate?: string; breed: string; status: "Disponível" | "Adotado" | "Em tratamento"; image?: string; @@ -19,16 +21,18 @@ export default function AnimalList() { const navigate = useNavigate(); useEffect(() => { - setTimeout(() => { - setAnimals([ - { id: 1, name: "Lilica", age: "2 anos", breed: "Poodle", status: "Disponível" }, - { id: 2, name: "Rex", age: "3 anos", breed: "Vira-lata", status: "Adotado" }, - { id: 3, name: "Bolt", age: "1 ano", breed: "Labrador", status: "Em tratamento" }, - { id: 4, name: "Mia", age: "4 anos", breed: "Siamês", status: "Disponível" }, - { id: 5, name: "Thor", age: "5 anos", breed: "Pastor Alemão", status: "Disponível" }, - ]); - setLoading(false); - }, 1000); + const fetchData = async () => { + try { + const data = await getAnimals(); + setAnimals(data as Animal[]); + } catch (error) { + console.error("Erro ao buscar animais:", error); + } finally { + setLoading(false); + } + }; + + fetchData(); }, []); const getStatusStyle = (status: Animal["status"]) => { @@ -39,14 +43,27 @@ export default function AnimalList() { } }; - const confirmDelete = () => { + const confirmDelete = async () => { if (deleteAnimal) { - setAnimals(prev => prev.filter(a => a.id !== deleteAnimal.id)); - if (selectedAnimal?.id === deleteAnimal.id) setSelectedAnimal(null); + await deleteAnimalById(deleteAnimal.id); + setAnimals((prev) => prev.filter((a) => a.id !== deleteAnimal.id)); setDeleteAnimal(null); + toast.success("Animal removido com sucesso!"); } }; + function calcularIdade(birthDate?: string): string { + if (!birthDate) return "Não informado"; + const nascimento = new Date(birthDate); + const hoje = new Date(); + let idade = hoje.getFullYear() - nascimento.getFullYear(); + const m = hoje.getMonth() - nascimento.getMonth(); + if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) { + idade--; + } + return `${idade} ano${idade !== 1 ? "s" : ""}`; + } + return (
@@ -115,8 +132,9 @@ export default function AnimalList() { {animal.status}
-

{animal.age} • {animal.breed}

-

{selectedAnimal.name}

-

Idade: {selectedAnimal.age}

+

Idade: {calcularIdade(selectedAnimal.birthDate)}

Raça: {selectedAnimal.breed}

Status:{" "} diff --git a/src/pages/AnimalRegister.tsx b/src/pages/AnimalRegister.tsx index 2443477..e7ed4d4 100644 --- a/src/pages/AnimalRegister.tsx +++ b/src/pages/AnimalRegister.tsx @@ -1,14 +1,14 @@ -import { useState } from "react"; +import React, { useState } from "react"; import { motion } from "framer-motion"; import { PawPrint, Save, Upload } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { registerAnimal } from "../services/animalService"; +import toast from "react-hot-toast"; export default function AnimalRegister() { const [preview, setPreview] = useState(null); const [imageFile, setImageFile] = useState(null); const [loading, setLoading] = useState(false); - const navigate = useNavigate(); const [formData, setFormData] = useState({ @@ -24,11 +24,14 @@ export default function AnimalRegister() { notes: "", }); + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + const handleChange = ( e: React.ChangeEvent ) => { const { name, value } = e.target; setFormData({ ...formData, [name]: value }); + setErrors({ ...errors, [name]: "" }); }; const handleImageChange = (e: React.ChangeEvent) => { @@ -36,26 +39,57 @@ export default function AnimalRegister() { if (file) { setPreview(URL.createObjectURL(file)); setImageFile(file); + setErrors({ ...errors, image: "" }); } }; + const validate = () => { + const newErrors: { [key: string]: string } = {}; + Object.entries(formData).forEach(([key, value]) => { + if (!value) newErrors[key] = "Campo obrigatório"; + }); + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setLoading(true); + if (!validate()) return; + setLoading(true); try { await registerAnimal(formData, imageFile || undefined); + setFormData({ + name: "", + birthDate: "", + sex: "", + color: "", + species: "", + breed: "", + rescueDate: "", + size: "", + status: "", + notes: "", + }); + setImageFile(null); + setPreview(null); + setErrors({}); + toast.success("Animal cadastrado com sucesso!"); } catch (err) { - alert("Erro ao cadastrar animal."); + console.error(err); + toast.error("Erro ao cadastrar animal."); } finally { setLoading(false); } }; - const inputModern = - "w-full px-4 py-3 rounded-xl bg-gray-100/70 shadow-sm " + - "focus:ring-2 focus:ring-blue-500 focus:outline-none " + - "transition placeholder-gray-400 text-gray-800"; + const inputModern = (error?: boolean) => + `w-full px-4 py-3 rounded-xl shadow-sm focus:ring-2 focus:outline-none transition placeholder-gray-400 text-gray-800 ` + + (error + ? "border-2 border-red-500 focus:ring-red-500 bg-red-50" + : "bg-gray-100/70 focus:ring-blue-500"); + + const errorStyle = "text-sm text-red-500 mt-1"; return (

@@ -78,15 +112,27 @@ export default function AnimalRegister() {

Informações do Animal

- - -
- - +
+ + + {errors.name && {errors.name}} +
+
+ + + {errors.birthDate && {errors.birthDate}} +
+
+ +
+ + +
+ {errors.sex && {errors.sex}}
@@ -94,48 +140,74 @@ export default function AnimalRegister() {

Características

- - - +
+ + + {errors.color && {errors.color}} +
+
+ + + {errors.species && {errors.species}} +
+
+ + + {errors.breed && {errors.breed}} +
- - - +
+ + + {errors.rescueDate && {errors.rescueDate}} +
+
+ + + {errors.size && {errors.size}} +
+
+ + + {errors.status && {errors.status}} +
-

Observações

-