Skip to content

Commit e4c63bc

Browse files
committed
[WEB] Albums - Add to Favorites #150, FE Development #168
1 parent c3ba2cd commit e4c63bc

File tree

11 files changed

+519
-62
lines changed

11 files changed

+519
-62
lines changed

src/api/api.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,38 @@ export async function emptyTrash(): Promise<boolean> {
367367

368368
return response.data;
369369
}
370+
371+
export async function addFavorites({
372+
objectIds,
373+
}: {
374+
objectIds: string[];
375+
}): Promise<number> {
376+
const response = await axiosClient.post('/object/addFavorites', {
377+
ObjectIds: objectIds,
378+
});
379+
380+
return response.data;
381+
}
382+
383+
export async function removeFavorites({
384+
objectIds,
385+
}: {
386+
objectIds: string[];
387+
}): Promise<number> {
388+
const response = await axiosClient.post('/object/removeFavorites', {
389+
ObjectIds: objectIds,
390+
});
391+
392+
return response.data;
393+
}
394+
395+
export const fetchFavoritesIds = async ({
396+
pageParam,
397+
}: {
398+
pageParam: string;
399+
}): Promise<IGetObjects> => {
400+
const res = await axiosClient.get('/objects/favorites', {
401+
params: { lastId: pageParam, PageSize: NUMBER_OF_OBJECTS_PER_PAGE },
402+
});
403+
return res.data;
404+
};
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import CancelIcon from '@mui/icons-material/Cancel';
2+
import CloseIcon from '@mui/icons-material/Close';
3+
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
4+
import {
5+
Button,
6+
Dialog,
7+
DialogActions,
8+
DialogTitle,
9+
Divider,
10+
Grid,
11+
Tooltip,
12+
Typography,
13+
} from '@mui/material';
14+
import AppBar from '@mui/material/AppBar';
15+
import Box from '@mui/material/Box';
16+
import IconButton from '@mui/material/IconButton';
17+
import Slide from '@mui/material/Slide';
18+
import { useTheme } from '@mui/material/styles';
19+
import Toolbar from '@mui/material/Toolbar';
20+
import {
21+
useInfiniteQuery,
22+
useMutation,
23+
useQueryClient,
24+
} from '@tanstack/react-query';
25+
import React, { useEffect, useState } from 'react';
26+
import toast from 'react-hot-toast';
27+
import { useInView } from 'react-intersection-observer';
28+
import { IThumbnail } from 'types/types';
29+
30+
import {
31+
fetchFavoritesIds,
32+
removeFavorites,
33+
} from '../api/api';
34+
import GalleryItemPaper from './GalleryItemPaper';
35+
import Loading from './Loading';
36+
import Preview from './Preview';
37+
38+
export const FavoritesGallery: React.FC = () => {
39+
const [previewOpen, setPreviewOpen] = useState(false);
40+
const [currentImage, setCurrentImage] = useState<number | null>(null);
41+
const [selectedImages, setSelectedImages] = useState<string[]>([]); // <-- add this
42+
const { ref, inView } = useInView();
43+
44+
const queryClient = useQueryClient();
45+
46+
const { data, fetchNextPage, hasNextPage, isLoading } = useInfiniteQuery({
47+
queryKey: ['fetchFavoritesIds'],
48+
queryFn: fetchFavoritesIds,
49+
initialPageParam: '',
50+
getNextPageParam: (lastPage) => lastPage.lastId || null,
51+
});
52+
53+
// mutation for restoring trashed objects
54+
const unfavoritesMutation = useMutation({
55+
mutationFn: removeFavorites,
56+
onSuccess: () => {
57+
toast.success('Object(s) removed from favorites successfully.');
58+
},
59+
onError: (error) => {
60+
toast.error(`Something went wrong: ${error.message}`);
61+
},
62+
});
63+
64+
const lastId = data?.pages.slice(-1)[0].lastId;
65+
const imageIds = data?.pages.map((page) => page.properties).flat();
66+
const lastImage = data?.pages.slice(-1)[0].properties?.slice(-1)[0].id;
67+
const numberOfImages = imageIds?.length || 0;
68+
const hasNewPage = hasNextPage && lastImage !== lastId;
69+
const hasImages = data?.pages && data?.pages[0].properties?.length > 0;
70+
71+
useEffect(() => {
72+
if (inView && hasNewPage) {
73+
fetchNextPage();
74+
}
75+
}, [inView]);
76+
77+
const openPreview = (index: number) => {
78+
setCurrentImage(index);
79+
setPreviewOpen(true);
80+
};
81+
82+
const closePreview = () => {
83+
setCurrentImage(null);
84+
setPreviewOpen(false);
85+
};
86+
87+
// if there is no previous image
88+
const disablePrevButton = currentImage === 0;
89+
90+
// if there is no next image
91+
const disableNextButton = currentImage === numberOfImages - 1 && !hasNewPage;
92+
93+
const handleNext = () => {
94+
if (disableNextButton) return;
95+
if (currentImage && currentImage >= numberOfImages - 2 && hasNewPage) {
96+
fetchNextPage();
97+
}
98+
setCurrentImage((prev) => (prev ? prev + 1 : 1));
99+
};
100+
101+
const handlePrev = () => {
102+
if (currentImage === 0) {
103+
return;
104+
}
105+
setCurrentImage((prev) => (prev ? prev - 1 : 0));
106+
};
107+
108+
const toggleSelectImage = (id: string) => {
109+
setSelectedImages((prev) =>
110+
prev.includes(id) ? prev.filter((imgId) => imgId !== id) : [...prev, id]
111+
);
112+
};
113+
114+
const handleClearSelection = () => setSelectedImages([]);
115+
116+
// tuka
117+
// and clear selection
118+
119+
const handleUnfavorites = () => {
120+
unfavoritesMutation.mutate(
121+
{ objectIds: selectedImages },
122+
{
123+
onSuccess: () => {
124+
handleClearSelection();
125+
queryClient.invalidateQueries({ queryKey: ['fetchFavoritesIds'] });
126+
},
127+
}
128+
);
129+
};
130+
131+
const theme = useTheme();
132+
133+
const [openUnfavoriteDialog, setOpenUnfavoriteDialog] = useState(false);
134+
135+
return (
136+
<>
137+
{/* Selection Toolbar */}
138+
<Slide
139+
direction="down"
140+
in={selectedImages.length > 0}
141+
mountOnEnter
142+
unmountOnExit
143+
>
144+
<AppBar
145+
position="fixed"
146+
color="default"
147+
elevation={2}
148+
sx={{
149+
top: 0,
150+
left: 0,
151+
right: 0,
152+
background: theme.palette.background.paper,
153+
borderBottom: '1px solid #eee',
154+
zIndex: theme.zIndex.drawer + 2,
155+
}}
156+
>
157+
<Toolbar>
158+
<IconButton
159+
edge="start"
160+
color="inherit"
161+
onClick={handleClearSelection}
162+
>
163+
<CloseIcon />
164+
</IconButton>
165+
<Typography sx={{ flexGrow: 1, fontWeight: 600 }}>
166+
{selectedImages.length} selected
167+
</Typography>
168+
<Tooltip title="Remove from Favorites">
169+
<IconButton
170+
color="inherit"
171+
onClick={() => setOpenUnfavoriteDialog(true)}
172+
>
173+
<CancelIcon />
174+
</IconButton>
175+
</Tooltip>
176+
<Dialog
177+
open={openUnfavoriteDialog}
178+
onClose={() => setOpenUnfavoriteDialog(false)}
179+
aria-labelledby="unfavorite-dialog-title"
180+
aria-describedby="unfavorite-dialog-description"
181+
>
182+
<DialogTitle
183+
id="unfavorite-dialog-title"
184+
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
185+
>
186+
<CancelIcon sx={{ fontSize: 32 }} />
187+
Remove from favororites {selectedImages.length}{' '}
188+
{selectedImages.length === 1 ? 'item' : 'items'}?
189+
</DialogTitle>
190+
<Typography
191+
id="unfavorite-dialog-description"
192+
sx={{ px: 3, pb: 1, color: 'text.secondary' }}
193+
>
194+
This action will remove from favororites the selected{' '}
195+
{selectedImages.length === 1 ? 'item' : 'items'}.
196+
</Typography>
197+
<DialogActions>
198+
<Button
199+
onClick={() => setOpenUnfavoriteDialog(false)}
200+
variant="outlined"
201+
>
202+
Cancel
203+
</Button>
204+
<Button
205+
onClick={() => {
206+
handleUnfavorites();
207+
setOpenUnfavoriteDialog(false);
208+
}}
209+
variant="contained"
210+
>
211+
Remove from Favorites
212+
</Button>
213+
</DialogActions>
214+
</Dialog>
215+
</Toolbar>
216+
</AppBar>
217+
</Slide>
218+
219+
<Divider sx={{ mb: 2 }} />
220+
<Box sx={{ mt: 3 }} />
221+
<Grid container spacing={1} columns={{ xs: 3, sm: 4, lg: 6, xl: 8 }}>
222+
{hasImages &&
223+
data.pages
224+
.map((page) => page.properties)
225+
.flat()
226+
.map((thumbnail: IThumbnail, index) => (
227+
<Grid
228+
item
229+
xs={1}
230+
key={thumbnail.id}
231+
sx={{ position: 'relative' }}
232+
>
233+
<GalleryItemPaper
234+
selected={selectedImages.includes(thumbnail.id)}
235+
onSelect={(e) => {
236+
e.stopPropagation();
237+
toggleSelectImage(thumbnail.id);
238+
}}
239+
onPreview={() => openPreview(index)}
240+
thumbnail={thumbnail}
241+
/>
242+
</Grid>
243+
))}
244+
</Grid>
245+
246+
{!isLoading && !hasImages && (
247+
<Typography color="text.secondary" gutterBottom sx={{ mt: 2 }}>
248+
<FavoriteBorderIcon sx={{ fontSize: 28, verticalAlign: 'middle' }} />{' '}
249+
No favorites
250+
</Typography>
251+
)}
252+
{isLoading && <Loading />}
253+
{currentImage !== null && imageIds && (
254+
<Preview
255+
isOpen={previewOpen}
256+
onClose={closePreview}
257+
handleNext={handleNext}
258+
handlePrev={handlePrev}
259+
media={imageIds[currentImage]}
260+
disablePrevButton={disablePrevButton}
261+
disableNextButton={disableNextButton}
262+
/>
263+
)}
264+
<Typography
265+
ref={ref}
266+
color="text.primary"
267+
gutterBottom
268+
sx={{ fontWeight: 600 }}
269+
></Typography>
270+
</>
271+
);
272+
};

0 commit comments

Comments
 (0)