Skip to content
This repository was archived by the owner on Oct 17, 2024. It is now read-only.

Commit 310f95c

Browse files
authored
Feat/listar acervo fixed (#189)
* Add: pages/Acervo * Up: add getItensAcervo to Utils/itemAcervoFirebase * Up: link to Acervo * Up: add router to Acervo * Add: Acervo tests * Adicionando data-cy 'card-item-nome' e 'item-privado'/'item-publico' * Refatorando Acervo e criando Acervo com getItensAcervo() mockado * Adicionando router para /acervo-mock (remover pra produção, mas inofensivo :]) * Refatorando testes
1 parent e7192b2 commit 310f95c

File tree

7 files changed

+637
-4
lines changed

7 files changed

+637
-4
lines changed

cypress/e2e/Acervo.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
describe('Acervo', () => {
2+
beforeEach(() => {
3+
cy.visit("/acervo-mock");
4+
});
5+
6+
it('deve renderizar o loading e os itens em caso de sucesso', () => {
7+
cy.get("[data-cy='loading']").should("exist");
8+
cy.get("[data-cy='loading']").should("not.exist");
9+
cy.get("[data-cy='card-item-container']").should("have.length", 6);
10+
});
11+
12+
it('deve carregar os últimos 6 itens em ordem decrescente', function() {
13+
cy.get("[data-cy='card-item-container']").then(($items) => {
14+
const ids = $items.map((_, el) => Number(el.getAttribute("data-id"))).get();
15+
const idsOrdenados = [...ids].sort((a, b) => b - a);
16+
expect(ids).to.deep.equal(idsOrdenados);
17+
});
18+
});
19+
20+
it('deve mostrar apenas itens públicos quando não logado', () => {
21+
cy.logout();
22+
cy.get("[data-cy='item-publico']").should('be.visible');
23+
cy.get("[data-cy='item-privado']").should('not.exist');
24+
});
25+
26+
it('deve mostrar itens privados quando logado', () => {
27+
cy.login();
28+
cy.visit("/acervo-mock");
29+
cy.get("[data-cy='card-item-container']").find("[data-cy='item-privado']").should("exist");
30+
});
31+
32+
it('não deve repetir itens na mesma página', () => {
33+
cy.get("[data-cy='card-item-container']").then(($items) => {
34+
const ids = $items.map((index, el) => el.getAttribute("data-id")).get();
35+
const idsUnicos = new Set(ids);
36+
expect(ids.length).to.equal(idsUnicos.size);
37+
});
38+
});
39+
});

src/Utils/itemAcervoFirebase.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
deleteDoc,
88
doc,
99
getDoc,
10+
getDocs,
1011
setDoc,
1112
updateDoc,
1213
} from "firebase/firestore";
@@ -25,6 +26,48 @@ import { FirebaseError } from "firebase/app";
2526
import { Colecao } from "../interfaces/Colecao";
2627
import Imagem from "../interfaces/Imagem";
2728

29+
const getItensAcervo = async (includePrivate: boolean = false): Promise<ItemAcervo[]> => {
30+
try {
31+
const itensAcervo: ItemAcervo[] = [];
32+
const colecoesRef = collection(db, "colecoes/publico/lista");
33+
const colecoesSnapshot = await getDocs(colecoesRef);
34+
35+
for (const colecaoDoc of colecoesSnapshot.docs) {
36+
const colecaoPath = colecaoDoc.ref.path;
37+
38+
// Busca itens públicos
39+
const itensPublicosRef = collection(db, `${colecaoPath}/publico`);
40+
const itensPublicosSnapshot = await getDocs(itensPublicosRef);
41+
42+
for (const itemDoc of itensPublicosSnapshot.docs) {
43+
const itemData = itemDoc.data() as ItemAcervo;
44+
itemData.id = itemDoc.id;
45+
itemData.colecao = colecaoDoc.id;
46+
itensAcervo.push(await getItemAcervo(itemDoc.ref.path));
47+
// itensAcervo.push(await processarImagens(itemData));
48+
}
49+
50+
// Busca itens privados se includePrivate for true
51+
if (includePrivate) {
52+
const itensPrivadosRef = collection(db, `${colecaoPath}/privado`);
53+
const itensPrivadosSnapshot = await getDocs(itensPrivadosRef);
54+
55+
for (const itemDoc of itensPrivadosSnapshot.docs) {
56+
const itemData = itemDoc.data() as ItemAcervo;
57+
itemData.id = itemDoc.id;
58+
itemData.colecao = colecaoDoc.id;
59+
itensAcervo.push(await getItemAcervo(itemDoc.ref.path));
60+
}
61+
}
62+
}
63+
64+
return itensAcervo;
65+
} catch (error) {
66+
console.error("Erro ao buscar itens do acervo:", error);
67+
throw new Error("Não foi possível carregar os itens do acervo");
68+
}
69+
};
70+
2871
const getItemAcervo = async (fullPath: string) => {
2972
try {
3073
const docRef: DocumentReference = doc(db, fullPath);
@@ -336,6 +379,7 @@ const methodsItemAcervo = {
336379
deleteItemAcervo,
337380
removerImagens,
338381
getItemAcervo,
382+
getItensAcervo,
339383
adicionarImagens,
340384
updateItemAcervo,
341385
getImagemItemAcervo,
@@ -346,6 +390,7 @@ export {
346390
methodsItemAcervo,
347391
deleteItemAcervo,
348392
getItemAcervo,
393+
getItensAcervo,
349394
updateItemAcervo,
350395
getImagemItemAcervo,
351396
};

src/components/CardItemAcervo/CardItemAcervo.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ const CardItemAcervo: React.FC<CardItemAcervoProps> = ({ item }) => {
107107
/>
108108
)}
109109
<CardContent>
110-
<Typography variant="body1" sx={{ fontSize: '20px' }}>
110+
<Typography variant="body1" sx={{ fontSize: '20px' }} data-cy="card-item-nome">
111111
{nome ? nome : 'Nome do item'}
112112
</Typography>
113-
<Typography variant="body2" sx={{ fontSize: '15px' }}>
113+
<Typography variant="body2" sx={{ fontSize: '15px' }} data-cy={privado ? 'item-privado' : 'item-publico'}>
114114
{privado ? 'Fora de exposição' : 'Em exposição'}
115115
</Typography>
116116
<Typography sx={{ marginTop: '15px', marginBottom: '15px' }} data-cy='card-item-description'>

src/components/NavBar/NavBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { loginMethods } from '../../Utils/loginGoogle'
2424
//Need to define links to other pages
2525
const pages = [
2626
{ label: "Exposições", sectionItem: "Criar exposição", seacrhLink: "/", otherLink: "exposicoes/criar-exposicao" },
27-
{ label: "Acervo", sectionItem: "Adicionar item", seacrhLink: "/", otherLink: "acervo/criar-item", otherSection: "Criar coleção", anotherLink: "colecoes/criar-colecao" },
27+
{ label: "Acervo", sectionItem: "Adicionar item", seacrhLink: "acervo", otherLink: "acervo/criar-item", otherSection: "Criar coleção", anotherLink: "colecoes/criar-colecao" },
2828
{ label: "Editais e normas", sectionItem: "Cadastrar normativa", seacrhLink: "/", otherLink: "/" },
2929
]
3030

src/pages/Acervo.tsx

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import React, { useState, useEffect, useCallback } from 'react';
2+
import {
3+
Box, Typography, TextField, IconButton, Grid, Pagination,
4+
CircularProgress, FormControl, InputLabel, MenuItem, Select,
5+
} from '@mui/material';
6+
import {
7+
Clear as ClearIcon,
8+
ArrowUpward as ArrowUpIcon,
9+
ArrowDownward as ArrowDownIcon,
10+
SortByAlpha
11+
} from '@mui/icons-material';
12+
import { styled } from '@mui/material/styles';
13+
import { CalendarIcon } from '@mui/x-date-pickers';
14+
import { onAuthStateChanged } from 'firebase/auth';
15+
import CardItemAcervo from '../components/CardItemAcervo/CardItemAcervo';
16+
import { ItemAcervo } from '../interfaces/ItemAcervo';
17+
import { auth } from '../../firebase/firebase';
18+
import { getItensAcervo } from '../Utils/itemAcervoFirebase';
19+
20+
const Content = styled(Box)(({ theme }) => ({
21+
padding: theme.spacing(3),
22+
display: 'flex',
23+
flexDirection: 'column',
24+
gap: theme.spacing(3)
25+
}));
26+
27+
const Heading = styled(Box)(() => ({
28+
textAlign: 'center'
29+
}));
30+
31+
const List = styled(Box)(({ theme }) => ({
32+
display: 'flex',
33+
flexDirection: 'column',
34+
gap: theme.spacing(2)
35+
}));
36+
37+
const Filter = styled(Box)(({ theme }) => ({
38+
display: 'flex',
39+
gap: theme.spacing(2)
40+
}));
41+
42+
const Description = styled(Box)(() => ({
43+
display: 'flex',
44+
justifyContent: 'space-between',
45+
alignItems: 'center'
46+
}));
47+
48+
const Acervo: React.FC = () => {
49+
const [itens, setItens] = useState<ItemAcervo[]>([]);
50+
const [filteredItens, setFilteredItens] = useState<ItemAcervo[]>([]);
51+
const [loading, setLoading] = useState(true);
52+
const [error, setError] = useState('');
53+
54+
const [nomeFilter, setNomeFilter] = useState('');
55+
const [colecaoFilter, setColecaoFilter] = useState('');
56+
const [colecoes, setColecoes] = useState<string[]>([]);
57+
58+
const [alfabeticOrder, setAlfabeticOrder] = useState<'asc' | 'desc' | 'none'>('none');
59+
const [dateOrder, setDateOrder] = useState<'asc' | 'desc' | 'none'>('desc');
60+
61+
const [page, setPage] = useState(1);
62+
const itemsPerPage = 6;
63+
64+
const [isLoggedIn, setIsLoggedIn] = useState(false);
65+
66+
useEffect(() => {
67+
const unsubscribe = onAuthStateChanged(auth, (user) => {
68+
setIsLoggedIn(!!user);
69+
});
70+
71+
return () => unsubscribe();
72+
}, []);
73+
74+
const fetchItens = useCallback(async () => {
75+
try {
76+
setLoading(true);
77+
const data = await getItensAcervo(isLoggedIn);
78+
setItens(data);
79+
setFilteredItens(data);
80+
const uniqueColecoes = Array.from(new Set(data.map(item => item.colecao)));
81+
setColecoes(uniqueColecoes);
82+
} catch (err) {
83+
setError('Erro ao buscar itens do acervo');
84+
} finally {
85+
// Add a slight delay to ensure the loading state is visible
86+
setTimeout(() => setLoading(false), 500);
87+
}
88+
}, [isLoggedIn]);
89+
90+
useEffect(() => {
91+
fetchItens();
92+
}, [fetchItens]);
93+
94+
useEffect(() => {
95+
let filtered = itens;
96+
97+
if (nomeFilter) {
98+
filtered = filtered.filter(item =>
99+
item.nome.toLowerCase().includes(nomeFilter.toLowerCase())
100+
);
101+
}
102+
103+
if (colecaoFilter) {
104+
filtered = filtered.filter(item => item.colecao === colecaoFilter);
105+
}
106+
107+
if (!isLoggedIn) {
108+
filtered = filtered.filter(item => !item.privado);
109+
}
110+
111+
if (alfabeticOrder !== 'none') {
112+
filtered.sort((a, b) => {
113+
return alfabeticOrder === 'asc'
114+
? a.nome.localeCompare(b.nome)
115+
: b.nome.localeCompare(a.nome);
116+
});
117+
} else if (dateOrder !== 'none') {
118+
filtered.sort((a, b) => {
119+
const dateA = a.dataDoacao instanceof Date ? a.dataDoacao.getTime() : 0;
120+
const dateB = b.dataDoacao instanceof Date ? b.dataDoacao.getTime() : 0;
121+
return dateOrder === 'asc' ? dateA - dateB : dateB - dateA;
122+
});
123+
}
124+
125+
setFilteredItens(filtered);
126+
setPage(1);
127+
}, [itens, nomeFilter, colecaoFilter, alfabeticOrder, dateOrder, isLoggedIn]);
128+
129+
const clearFilters = useCallback(() => {
130+
setNomeFilter('');
131+
setColecaoFilter('');
132+
setAlfabeticOrder('none');
133+
setDateOrder('desc');
134+
}, []);
135+
136+
const toggleAlfabeticOrder = useCallback(() => {
137+
setAlfabeticOrder(prev => {
138+
if (prev === 'asc') return 'desc';
139+
if (prev === 'desc') return 'none';
140+
return 'asc';
141+
});
142+
setDateOrder('none');
143+
setPage(1);
144+
}, []);
145+
146+
const toggleDateOrder = useCallback(() => {
147+
setDateOrder(prev => {
148+
if (prev === 'asc') return 'desc';
149+
if (prev === 'desc') return 'none';
150+
return 'asc';
151+
});
152+
setAlfabeticOrder('none');
153+
setPage(1);
154+
}, []);
155+
156+
const handlePageChange = useCallback((event: React.ChangeEvent<unknown>, value: number) => {
157+
setPage(value);
158+
void(event)
159+
}, []);
160+
161+
if (loading) {
162+
return <CircularProgress data-cy="loading" />;
163+
}
164+
165+
if (error) {
166+
return <Typography color="error" data-cy="error-message">{error}</Typography>;
167+
}
168+
169+
const paginatedItens = filteredItens.slice(
170+
(page - 1) * itemsPerPage,
171+
page * itemsPerPage
172+
);
173+
174+
return (
175+
<Content data-cy="acervo-content">
176+
<Heading>
177+
<Typography variant="h4" sx={{paddingTop: '32px'}} data-cy="acervo-title">Acervo</Typography>
178+
<Typography variant="subtitle1" data-cy="acervo-subtitle">Explore os itens do nosso acervo</Typography>
179+
</Heading>
180+
181+
<List sx={{
182+
display: 'flex',
183+
alignItems: 'center',
184+
}}
185+
>
186+
<Filter>
187+
<TextField
188+
label="Nome do item"
189+
value={nomeFilter}
190+
onChange={(e) => setNomeFilter(e.target.value)}
191+
inputProps={{ 'data-cy': 'nome-filter' }}
192+
/>
193+
<FormControl sx={{minWidth: '150px'}}>
194+
<InputLabel>Coleção</InputLabel>
195+
<Select
196+
value={colecaoFilter}
197+
onChange={(e) => setColecaoFilter(e.target.value as string)}
198+
label="Coleção"
199+
inputProps={{ 'data-cy': 'colecao-filter' }}
200+
>
201+
<MenuItem value="">Todas</MenuItem>
202+
{colecoes.map((colecao) => (
203+
<MenuItem key={colecao} value={colecao}>{colecao}</MenuItem>
204+
))}
205+
</Select>
206+
</FormControl>
207+
</Filter>
208+
209+
<Description>
210+
<Box>
211+
<IconButton onClick={toggleAlfabeticOrder} data-cy="alfabetic-order">
212+
<SortByAlpha />
213+
{alfabeticOrder === 'asc' ? <ArrowUpIcon /> : alfabeticOrder === 'desc' ? <ArrowDownIcon /> : null}
214+
</IconButton>
215+
<IconButton onClick={toggleDateOrder} data-cy="date-order">
216+
<CalendarIcon />
217+
{dateOrder === 'asc' ? <ArrowUpIcon /> : dateOrder === 'desc' ? <ArrowDownIcon /> : null}
218+
</IconButton>
219+
<IconButton onClick={clearFilters} data-cy="clear-filters">
220+
<ClearIcon />
221+
</IconButton>
222+
</Box>
223+
<Typography data-cy="items-count">
224+
{filteredItens.length} itens encontrados
225+
</Typography>
226+
</Description>
227+
228+
<Pagination
229+
count={Math.ceil(filteredItens.length / itemsPerPage)}
230+
page={page}
231+
onChange={handlePageChange}
232+
data-cy="pagination"
233+
/>
234+
235+
{filteredItens.length === 0 ? (
236+
<Typography data-cy="no-items-message">Nenhum item encontrado com os filtros atuais</Typography>
237+
) : (
238+
<>
239+
<Grid container spacing={2} data-cy="items-grid">
240+
{paginatedItens.map(item => (
241+
<Grid item xs={12} sm={6} md={4} key={item.id}>
242+
<CardItemAcervo item={item} data-cy="item-card" data-id={item.id} />
243+
</Grid>
244+
))}
245+
</Grid>
246+
<Pagination
247+
count={Math.ceil(filteredItens.length / itemsPerPage)}
248+
page={page}
249+
onChange={handlePageChange}
250+
data-cy="pagination"
251+
/>
252+
</>
253+
)}
254+
</List>
255+
</Content>
256+
);
257+
};
258+
259+
export default Acervo;

0 commit comments

Comments
 (0)