Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/Components/ProtectedSkeleton/AnimalListSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
interface Props {
count?: number;
}

export default function AnimalListSkeleton({ count = 8 }: Props) {
const items = Array.from({ length: count });

return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{items.map((_, idx) => (
<div
key={idx}
className="rounded-2xl shadow-md bg-white overflow-hidden animate-pulse"
aria-hidden="true"
>
<div className="h-40 bg-gray-200/80"></div>
<div className="p-4">
<div className="h-5 bg-gray-200/80 rounded w-3/4 mb-3"></div>
<div className="h-4 bg-gray-200/80 rounded w-1/2 mb-4"></div>

<div className="flex items-center justify-between gap-3">
<div className="h-6 bg-gray-200/80 rounded w-24"></div>
<div className="h-8 bg-gray-200/80 rounded w-28"></div>
</div>
</div>
</div>
))}
</div>
);
}
46 changes: 38 additions & 8 deletions src/context/AnimalsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContext, useContext, useState, useEffect, type ReactNode } from "react";
import { collection, onSnapshot, doc, updateDoc } from "firebase/firestore";
import { collection, onSnapshot, doc, updateDoc, getDocs } from "firebase/firestore";
import { db } from "../lib/firebase.ts";

export type Animal = {
Expand All @@ -22,6 +22,8 @@ export type Animal = {

type AnimalsContextType = {
animals: Animal[];
loading: boolean;
refresh: () => Promise<void>;
updateAnimalStatus: (id: string, status: Animal["status"]) => Promise<void>;
removeAnimalFromContext: (id: string) => void;
markAnimalAsAdopted: (id: string) => Promise<void>;
Expand All @@ -32,25 +34,51 @@ const AnimalsContext = createContext<AnimalsContextType | undefined>(undefined);

export const AnimalsProvider = ({ children }: { children: ReactNode }) => {
const [animals, setAnimals] = useState<Animal[]>([]);
const [loading, setLoading] = useState<boolean>(true);

useEffect(() => {
const animalsCol = collection(db, "animals");
setLoading(true);

const unsubscribe = onSnapshot(animalsCol, (snapshot) => {
const formatted = snapshot.docs.map((doc) => ({
const unsubscribe = onSnapshot(
animalsCol,
(snapshot) => {
const formatted = snapshot.docs.map((doc) => ({
id: doc.id,
...(doc.data() as Omit<Animal, "id">),
}));
setAnimals(formatted);
setLoading(false);
},
(error) => {
console.error("Erro no snapshot de animals:", error);
setLoading(false);
}
);

return () => unsubscribe();
}, []);

const refresh = async () => {
setLoading(true);
try {
const animalsCol = collection(db, "animals");
const snap = await getDocs(animalsCol);
const formatted = snap.docs.map((doc) => ({
id: doc.id,
...(doc.data() as Omit<Animal, "id">),
}));
setAnimals(formatted);
});

return () => unsubscribe();
}, []);
} catch (err) {
console.error("Erro ao atualizar animais:", err);
} finally {
setLoading(false);
}
};

const updateAnimalStatus = async (id: string, status: Animal["status"]) => {
const docRef = doc(db, "animals", id);
await updateDoc(docRef, { status });

setAnimals(prev => prev.map(a => a.id === id ? { ...a, status } : a));
};

Expand All @@ -69,6 +97,8 @@ export const AnimalsProvider = ({ children }: { children: ReactNode }) => {
return (
<AnimalsContext.Provider value={{
animals,
loading,
refresh,
updateAnimalStatus,
removeAnimalFromContext,
markAnimalAsAdopted,
Expand Down
156 changes: 112 additions & 44 deletions src/pages/Animal/AnimalList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useState } from "react";
// src/pages/Animal/AnimalList.tsx
import React, { useState, useEffect } from "react";
import { Plus, X, Trash2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast";
import { deleteAnimalById, updateAnimalById } from "../../services/animalService.ts";
import { motion, AnimatePresence } from "framer-motion";
import { useAnimals, type Animal } from "../../context/AnimalsContext.tsx";
import AnimalListSkeleton from "../../Components/ProtectedSkeleton/AnimalListSkeleton.tsx";

export default function AnimalList() {
const [editForm, setEditForm] = useState<Animal | null>(null);
Expand All @@ -14,15 +16,31 @@ export default function AnimalList() {
const [searchTerm, setSearchTerm] = useState("");
const [selectedAnimal, setSelectedAnimal] = useState<Animal | null>(null);

const [currentPage, setCurrentPage] = useState(1);
const pageSize = 8;

const navigate = useNavigate();
const { animals, updateAnimalStatus, removeAnimalFromContext } = useAnimals();
const { animals, loading, updateAnimalStatus, removeAnimalFromContext } = useAnimals();

const filteredAnimals = animals.filter(
a =>
a.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
a.breed?.toLowerCase().includes(searchTerm.toLowerCase())
);

const totalPages = Math.max(1, Math.ceil(filteredAnimals.length / pageSize));
useEffect(() => {
if (currentPage > totalPages) setCurrentPage(totalPages);
}, [totalPages, currentPage]);

useEffect(() => {
setCurrentPage(1);
}, [searchTerm]);

const startIndex = (currentPage - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize, filteredAnimals.length);
const paginatedAnimals = filteredAnimals.slice(startIndex, endIndex);

const getStatusStyle = (status: Animal["status"]) => {
switch (status) {
case "Disponível":
Expand Down Expand Up @@ -107,6 +125,15 @@ export default function AnimalList() {
}
};

const getPageNumbers = () => {
const pages: number[] = [];
let start = Math.max(1, currentPage - 2);
const end = Math.min(totalPages, start + 4);
if (end - start < 4) start = Math.max(1, end - 4);
for (let i = start; i <= end; i++) pages.push(i);
return pages;
};

return (
<div className="p-6 flex flex-col gap-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
Expand All @@ -127,55 +154,96 @@ export default function AnimalList() {
</div>
</div>

{filteredAnimals.length === 0 ? (
{loading ? (
<AnimalListSkeleton count={pageSize} />
) : filteredAnimals.length === 0 ? (
<div className="text-center text-gray-500 py-20">Nenhum animal encontrado.</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{filteredAnimals.map(animal => (
<div
key={animal.id}
className="rounded-2xl shadow-md hover:shadow-xl transition bg-white overflow-hidden group"
>
<div className="h-40 bg-gray-200 flex items-center justify-center relative">
<span className="text-gray-400 group-hover:opacity-0 transition">Imagem</span>
{animal.image && (
<img
src={animal.image}
alt={animal.name}
className="absolute inset-0 w-full h-full object-cover"
/>
)}
<button
onClick={() => setDeleteAnimal(animal)}
className="absolute top-2 right-2 text-red-500 bg-white p-1 rounded-full hover:bg-red-50 transition"
>
<Trash2 size={16} />
</button>
</div>
<div className="p-4 flex flex-col gap-2">
<div className="flex justify-between items-center">
<h2 className="font-semibold text-gray-800 text-lg">{animal.name}</h2>
<span
className={`text-xs px-2 py-1 rounded-full font-medium ${getStatusStyle(
animal.status
)}`}
<>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{paginatedAnimals.map(animal => (
<div
key={animal.id}
className="rounded-2xl shadow-md hover:shadow-xl transition bg-white overflow-hidden group"
>
<div className="h-40 bg-gray-200 flex items-center justify-center relative">
<span className="text-gray-400 group-hover:opacity-0 transition">Imagem</span>
{animal.image && (
<img
src={animal.image}
alt={animal.name}
className="absolute inset-0 w-full h-full object-cover"
/>
)}
<button
onClick={() => setDeleteAnimal(animal)}
className="absolute top-2 right-2 text-red-500 bg-white p-1 rounded-full hover:bg-red-50 transition"
>
{animal.status}
</span>
<Trash2 size={16} />
</button>
</div>
<p className="text-sm text-gray-600">
{calcularIdade(animal.birthDate)} • {animal.breed || "-"}
</p>
<div className="p-4 flex flex-col gap-2">
<div className="flex justify-between items-center">
<h2 className="font-semibold text-gray-800 text-lg">{animal.name}</h2>
<span
className={`text-xs px-2 py-1 rounded-full font-medium ${getStatusStyle(
animal.status
)}`}
>
{animal.status}
</span>
</div>
<p className="text-sm text-gray-600">
{calcularIdade(animal.birthDate)} • {animal.breed || "-"}
</p>
<button
onClick={() => setSelectedAnimal(animal)}
className="w-full mt-2 px-3 py-2 border border-gray-200 rounded-xl hover:bg-gray-100 transition text-sm font-medium"
>
Ver detalhes
</button>
</div>
</div>
))}
</div>

<div className="flex flex-col gap-5 items-center justify-between mt-6">
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
aria-disabled={currentPage === 1}
className={`px-3 py-1 rounded-md border border-gray-300 ${currentPage === 1 ? "cursor-not-allowed opacity-50" : "cursor-pointer hover:bg-gray-100"}`}
>
Anterior
</button>

{getPageNumbers().map(n => (
<button
onClick={() => setSelectedAnimal(animal)}
className="w-full mt-2 px-3 py-2 border border-gray-200 rounded-xl hover:bg-gray-100 transition text-sm font-medium"
key={n}
onClick={() => setCurrentPage(n)}
aria-current={n === currentPage ? "page" : undefined}
className={`px-3 py-1 rounded-md border ${n === currentPage ? "bg-blue-600 text-white border-blue-600" : "border-gray-300 hover:bg-gray-100"} cursor-pointer`}
>
Ver detalhes
{n}
</button>
</div>
))}

<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
aria-disabled={currentPage === totalPages}
className={`px-3 py-1 rounded-md border border-gray-300 ${currentPage === totalPages ? "cursor-not-allowed opacity-50" : "cursor-pointer hover:bg-gray-100"}`}
>
Próxima
</button>
</div>
))}
</div>

<div className="text-sm text-gray-600">
Mostrando {startIndex + 1}–{endIndex} de {filteredAnimals.length}
</div>
</div>
</>
)}

<AnimatePresence>
Expand Down Expand Up @@ -429,4 +497,4 @@ export default function AnimalList() {
)}
</div>
);
}
}
20 changes: 16 additions & 4 deletions src/pages/Animal/AnimalRegister.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,24 @@ export default function AnimalRegister() {
<label className="mb-1 font-medium text-gray-700">Sexo</label>
<div className="flex flex-wrap gap-4 items-center mt-1">
<label className="flex items-center gap-2 text-gray-700">
<input type="radio" name="sex" value="M" onChange={handleChange}
className="accent-blue-600"/> Macho
<input
type="radio"
name="sex"
value="M"
checked={formData.sex === "M"}
onChange={handleChange}
className="accent-blue-600"
/> Macho
</label>
<label className="flex items-center gap-2 text-gray-700">
<input type="radio" name="sex" value="F" onChange={handleChange}
className="accent-blue-600"/> Fêmea
<input
type="radio"
name="sex"
value="F"
checked={formData.sex === "F"}
onChange={handleChange}
className="accent-blue-600"
/> Fêmea
</label>
</div>
{errors.sex && <span className={errorStyle}>{errors.sex}</span>}
Expand Down