Skip to content

Commit 367ad35

Browse files
authored
Merge pull request #20 from scalefocus/fix/175_favorites_removal_and_adding_improvements
Favorites removal and adding improvements
2 parents 9589938 + 481c7ec commit 367ad35

File tree

7 files changed

+326
-156
lines changed

7 files changed

+326
-156
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import FavoriteIcon from '@mui/icons-material/Favorite';
2+
import IconButton from '@mui/material/IconButton';
3+
import React, { useState } from 'react';
4+
5+
interface FavoriteMediaButtonProps {
6+
thumbnail: {
7+
id: string;
8+
mediaType?: string;
9+
isFavorite?: boolean;
10+
};
11+
handleSingleFavorites?: (id: string, actionAdd: boolean) => void;
12+
}
13+
14+
const FavoriteMediaButton: React.FC<FavoriteMediaButtonProps> = ({
15+
thumbnail,
16+
handleSingleFavorites,
17+
}) => {
18+
const [hovered, setHovered] = useState(false);
19+
20+
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement>) => {
21+
e.stopPropagation();
22+
handleSingleFavorites?.(thumbnail.id, !thumbnail.isFavorite);
23+
};
24+
25+
return (
26+
<IconButton
27+
size="small"
28+
onClick={handleOnClick}
29+
onMouseEnter={() => setHovered(true)}
30+
onMouseLeave={() => setHovered(false)}
31+
sx={{
32+
position: 'absolute',
33+
bottom: 9,
34+
left: 7,
35+
zIndex: 2,
36+
}}
37+
>
38+
{(() => {
39+
if (hovered && thumbnail.isFavorite != null && !thumbnail.isFavorite) {
40+
return <FavoriteIcon sx={{ color: 'white', fontSize: 24 }} />; // red when hovered and not favorite
41+
} else {
42+
return <FavoriteIcon sx={{ opacity: 0, fontSize: 24 }} />;
43+
}
44+
})()}
45+
</IconButton>
46+
);
47+
};
48+
49+
export default FavoriteMediaButton;

src/components/FavoritesGallery.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import CancelIcon from '@mui/icons-material/Cancel';
22
import CloseIcon from '@mui/icons-material/Close';
33
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
4+
import HeartBrokenIcon from '@mui/icons-material/HeartBroken';
45
import {
56
Button,
67
Dialog,
@@ -27,10 +28,7 @@ import toast from 'react-hot-toast';
2728
import { useInView } from 'react-intersection-observer';
2829
import { IThumbnail } from 'types/types';
2930

30-
import {
31-
fetchFavoritesIds,
32-
removeFavorites,
33-
} from '../api/api';
31+
import { fetchFavoritesIds, removeFavorites } from '../api/api';
3432
import GalleryItemPaper from './GalleryItemPaper';
3533
import Loading from './Loading';
3634
import Preview from './Preview';
@@ -128,6 +126,17 @@ export const FavoritesGallery: React.FC = () => {
128126
);
129127
};
130128

129+
const handleSingleFavorites = (id: string) => {
130+
unfavoritesMutation.mutate(
131+
{ objectIds: [id] },
132+
{
133+
onSuccess: () => {
134+
queryClient.invalidateQueries({ queryKey: ['fetchFavoritesIds'] });
135+
},
136+
}
137+
);
138+
};
139+
131140
const theme = useTheme();
132141

133142
const [openUnfavoriteDialog, setOpenUnfavoriteDialog] = useState(false);
@@ -170,7 +179,7 @@ export const FavoritesGallery: React.FC = () => {
170179
color="inherit"
171180
onClick={() => setOpenUnfavoriteDialog(true)}
172181
>
173-
<CancelIcon />
182+
<HeartBrokenIcon />
174183
</IconButton>
175184
</Tooltip>
176185
<Dialog
@@ -238,6 +247,7 @@ export const FavoritesGallery: React.FC = () => {
238247
}}
239248
onPreview={() => openPreview(index)}
240249
thumbnail={thumbnail}
250+
handleSingleFavorites={handleSingleFavorites}
241251
/>
242252
</Grid>
243253
))}
Lines changed: 105 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,120 @@
1-
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
1+
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
2+
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
23
import Box from '@mui/material/Box';
34
import Paper from '@mui/material/Paper';
45
import React from 'react';
56

7+
import FavoriteMediaButton from './FavoriteMediaButton';
68
import ImageThumbnail from './ImageThumbnail';
79
import SelectImageButton from './SelectImageButton';
810

911
interface GalleryItemPaperProps {
10-
selected: boolean;
11-
onSelect: (e: React.MouseEvent<HTMLButtonElement>) => void;
12-
onPreview: () => void;
13-
thumbnail: {
14-
id: string;
15-
mediaType?: string;
16-
isFavorite?: boolean;
17-
};
12+
selected: boolean;
13+
onSelect: (e: React.MouseEvent<HTMLButtonElement>) => void;
14+
onPreview: () => void;
15+
thumbnail: {
16+
id: string;
17+
mediaType?: string;
18+
isFavorite?: boolean;
19+
};
20+
handleSingleFavorites?: (id: string, actionAdd: boolean) => void;
1821
}
1922

2023
const GalleryItemPaper: React.FC<GalleryItemPaperProps> = ({
21-
selected,
22-
onSelect,
23-
onPreview,
24-
thumbnail,
24+
selected,
25+
onSelect,
26+
onPreview,
27+
thumbnail,
28+
handleSingleFavorites,
2529
}) => (
26-
<Paper elevation={0}>
27-
<SelectImageButton
28-
selected={selected}
29-
onClick={onSelect}
30-
/>
31-
<div onClick={onPreview}>
32-
{selected ? (
33-
<Box component="section" sx={{ p: 1.5 }}>
34-
<ImageThumbnail
35-
id={thumbnail.id}
36-
mediaType={thumbnail.mediaType}
37-
isFavorite={thumbnail?.isFavorite}
38-
/>
39-
</Box>
40-
) : (
41-
<Box
42-
component="section"
43-
sx={{
44-
position: 'relative',
45-
'&:hover .check-icon': {
46-
opacity: 1,
47-
},
48-
'&:hover .selection-gradient': {
49-
opacity: 1,
50-
},
51-
}}
52-
>
53-
<ImageThumbnail
54-
id={thumbnail.id}
55-
mediaType={thumbnail.mediaType}
56-
isFavorite={thumbnail?.isFavorite}
57-
/>
58-
{/* Gradient overlay, appears on hover */}
59-
<Box
60-
className="selection-gradient"
61-
sx={{
62-
position: 'absolute',
63-
top: 0,
64-
left: 0,
65-
width: '100%',
66-
height: '50%',
67-
pointerEvents: 'none',
68-
opacity: 0,
69-
transition: 'opacity 0.2s',
70-
zIndex: 2,
71-
background:
72-
'linear-gradient(to bottom, rgba(26,115,232,0.18) 0%, rgba(26,115,232,0.01) 100%)',
73-
borderTopLeftRadius: 8,
74-
borderTopRightRadius: 8,
75-
}}
76-
/>
77-
<Box
78-
className="check-icon"
79-
sx={{
80-
position: 'absolute',
81-
top: 7,
82-
left: 3,
83-
opacity: 0,
84-
transition: 'opacity 0.2s',
85-
zIndex: 3,
86-
pointerEvents: 'none',
87-
}}
88-
>
89-
<CheckCircleIcon
90-
sx={{ color: 'white', fontSize: 24, opacity: 0.5 }}
91-
/>
92-
</Box>
93-
</Box>
94-
)}
95-
</div>
96-
</Paper>
30+
<Paper elevation={0}>
31+
<SelectImageButton selected={selected} onClick={onSelect} />
32+
<FavoriteMediaButton
33+
thumbnail={thumbnail}
34+
handleSingleFavorites={handleSingleFavorites}
35+
/>
36+
<div onClick={onPreview}>
37+
{selected ? (
38+
<Box component="section" sx={{ p: 1.5 }}>
39+
<ImageThumbnail
40+
id={thumbnail.id}
41+
mediaType={thumbnail.mediaType}
42+
isFavorite={thumbnail?.isFavorite}
43+
/>
44+
</Box>
45+
) : (
46+
<Box
47+
component="section"
48+
sx={{
49+
position: 'relative',
50+
'&:hover .check-icon': {
51+
opacity: 1,
52+
},
53+
'&:hover .selection-gradient': {
54+
opacity: 1,
55+
},
56+
}}
57+
>
58+
<ImageThumbnail
59+
id={thumbnail.id}
60+
mediaType={thumbnail.mediaType}
61+
isFavorite={thumbnail?.isFavorite}
62+
/>
63+
{/* Gradient overlay, appears on hover */}
64+
<Box
65+
className="selection-gradient"
66+
sx={{
67+
position: 'absolute',
68+
top: 0,
69+
left: 0,
70+
width: '90%',
71+
height: '90%',
72+
pointerEvents: 'none',
73+
opacity: 0,
74+
transition: 'opacity 0.2s',
75+
zIndex: 2,
76+
background:
77+
'linear-gradient(to right, rgba(26,115,232,0.18) 0%, rgba(26,115,232,0.01) 100%)',
78+
borderTopLeftRadius: 8,
79+
borderTopRightRadius: 8,
80+
}}
81+
/>
82+
<Box
83+
className="check-icon"
84+
sx={{
85+
position: 'absolute',
86+
top: 7,
87+
left: 3,
88+
opacity: 0,
89+
transition: 'opacity 0.2s',
90+
zIndex: 3,
91+
pointerEvents: 'none',
92+
}}
93+
>
94+
<CheckCircleOutlineIcon
95+
sx={{ color: 'white', fontSize: 24, opacity: 0.8 }}
96+
/>
97+
</Box>
98+
{thumbnail.isFavorite != null && !thumbnail.isFavorite && (
99+
<Box
100+
className="check-icon"
101+
sx={{
102+
position: 'absolute',
103+
bottom: 8,
104+
left: 4,
105+
opacity: 0,
106+
transition: 'opacity 0.2s',
107+
zIndex: 3,
108+
pointerEvents: 'none',
109+
}}
110+
>
111+
<FavoriteBorderIcon sx={{ color: 'white', fontSize: 24, opacity: 0.8 }} />{' '} {/* yellow when not favorite */}
112+
</Box>
113+
)}
114+
</Box>
115+
)}
116+
</div>
117+
</Paper>
97118
);
98119

99120
export default GalleryItemPaper;

src/components/ImageGallery.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ import toast from 'react-hot-toast';
2727
import { useInView } from 'react-intersection-observer';
2828
import { IThumbnail } from 'types/types';
2929

30-
import { addFavorites, downloadObjectsAsZip, fetchImageIds, trashObjects } from '../api/api';
30+
import {
31+
addFavorites,
32+
downloadObjectsAsZip,
33+
fetchImageIds,
34+
removeFavorites,
35+
trashObjects,
36+
} from '../api/api';
3137
import GalleryItemPaper from './GalleryItemPaper';
3238
import Loading from './Loading';
3339
import Preview from './Preview';
@@ -80,6 +86,14 @@ export const ImageGallery: React.FC = () => {
8086
},
8187
});
8288

89+
const singleAddFavoritesMutation = useMutation({
90+
mutationFn: addFavorites,
91+
});
92+
93+
const singleRemoveFavoritesMutation = useMutation({
94+
mutationFn: removeFavorites,
95+
});
96+
8397
const lastId = data?.pages.slice(-1)[0].lastId;
8498
const imageIds = data?.pages.map((page) => page.properties).flat();
8599
const lastImage = data?.pages.slice(-1)[0].properties?.slice(-1)[0].id;
@@ -179,6 +193,28 @@ export const ImageGallery: React.FC = () => {
179193
}
180194
);
181195
};
196+
const handleSingleFavorites = (id: string, actionAdd: boolean) => {
197+
if (actionAdd) {
198+
singleAddFavoritesMutation.mutate(
199+
{ objectIds: [id] },
200+
{
201+
onSuccess: () => {
202+
queryClient.invalidateQueries({ queryKey: ['fetchIds'] });
203+
},
204+
}
205+
);
206+
return;
207+
}
208+
209+
singleRemoveFavoritesMutation.mutate(
210+
{ objectIds: [id] },
211+
{
212+
onSuccess: () => {
213+
queryClient.invalidateQueries({ queryKey: ['fetchIds'] });
214+
},
215+
}
216+
);
217+
};
182218
const theme = useTheme();
183219

184220
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
@@ -309,6 +345,7 @@ export const ImageGallery: React.FC = () => {
309345
}}
310346
onPreview={() => openPreview(index)}
311347
thumbnail={thumbnail}
348+
handleSingleFavorites={handleSingleFavorites}
312349
/>
313350
</Grid>
314351
))}
@@ -329,6 +366,7 @@ export const ImageGallery: React.FC = () => {
329366
media={imageIds[currentImage]}
330367
disablePrevButton={disablePrevButton}
331368
disableNextButton={disableNextButton}
369+
handleSingleFavorites={handleSingleFavorites}
332370
/>
333371
)}
334372
<Typography

0 commit comments

Comments
 (0)