Skip to content

Commit 0363b68

Browse files
committed
Add trash functionality and selection UI to ImageGallery component
1 parent 7f22dc0 commit 0363b68

File tree

3 files changed

+267
-6
lines changed

3 files changed

+267
-6
lines changed

src/api/api.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,16 @@ export async function adminResetPassword({
297297

298298
return response.data;
299299
}
300+
301+
export async function trashObjects({
302+
objectIds,
303+
}: {
304+
objectIds: string[];
305+
}): Promise<number> {
306+
307+
const response = await axiosClient.post('object/trash', {
308+
ObjectIds : objectIds,
309+
});
310+
311+
return response.data;
312+
}

src/components/ImageGallery.tsx

Lines changed: 187 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,64 @@
1-
import { Divider, Grid, Paper, Typography } from '@mui/material';
2-
import { useInfiniteQuery } from '@tanstack/react-query';
1+
import CloseIcon from '@mui/icons-material/Close';
2+
import DeleteIcon from '@mui/icons-material/Delete';
3+
import DownloadIcon from '@mui/icons-material/Download';
4+
import {
5+
Button,
6+
Dialog,
7+
DialogActions,
8+
DialogTitle,
9+
Divider,
10+
Grid,
11+
Paper,
12+
Tooltip,
13+
Typography,
14+
} from '@mui/material';
15+
import AppBar from '@mui/material/AppBar';
16+
import Box from '@mui/material/Box';
17+
import IconButton from '@mui/material/IconButton';
18+
import Slide from '@mui/material/Slide';
19+
import { useTheme } from '@mui/material/styles';
20+
import Toolbar from '@mui/material/Toolbar';
21+
import {
22+
useInfiniteQuery,
23+
useMutation,
24+
useQueryClient,
25+
} from '@tanstack/react-query';
326
import React, { useEffect, useState } from 'react';
27+
import toast from 'react-hot-toast';
428
import { useInView } from 'react-intersection-observer';
529
import { IThumbnail } from 'types/types';
630

7-
import { fetchImageIds } from '../api/api';
31+
import { fetchImageIds, trashObjects } from '../api/api';
832
import ImageThumbnail from './ImageThumbnail';
933
import Loading from './Loading';
1034
import Preview from './Preview';
35+
import SelectImageButton from './SelectImageButton';
1136

1237
export const ImageGallery: React.FC = () => {
1338
const [previewOpen, setPreviewOpen] = useState(false);
1439
const [currentImage, setCurrentImage] = useState<number | null>(null);
40+
const [selectedImages, setSelectedImages] = useState<string[]>([]); // <-- add this
1541
const { ref, inView } = useInView();
1642

43+
const queryClient = useQueryClient();
44+
1745
const { data, fetchNextPage, hasNextPage, isLoading } = useInfiniteQuery({
1846
queryKey: ['fetchIds'],
1947
queryFn: fetchImageIds,
2048
initialPageParam: '',
2149
getNextPageParam: (lastPage) => lastPage.lastId || null,
2250
});
2351

52+
const trashObjectMutation = useMutation({
53+
mutationFn: trashObjects,
54+
onSuccess: () => {
55+
toast.success('Object(s) trashed successfully.');
56+
},
57+
onError: (error) => {
58+
toast.error(`Something went wrong: ${error.message}`);
59+
},
60+
});
61+
2462
const lastId = data?.pages.slice(-1)[0].lastId;
2563
const imageIds = data?.pages.map((page) => page.properties).flat();
2664
const lastImage = data?.pages.slice(-1)[0].properties?.slice(-1)[0].id;
@@ -65,8 +103,125 @@ export const ImageGallery: React.FC = () => {
65103
setCurrentImage((prev) => (prev ? prev - 1 : 0));
66104
};
67105

106+
const toggleSelectImage = (id: string) => {
107+
setSelectedImages((prev) =>
108+
prev.includes(id) ? prev.filter((imgId) => imgId !== id) : [...prev, id]
109+
);
110+
};
111+
112+
const handleClearSelection = () => setSelectedImages([]);
113+
const handleDelete = () => {
114+
trashObjectMutation.mutate(
115+
{ objectIds: selectedImages },
116+
{
117+
onSuccess: () => {
118+
handleClearSelection();
119+
queryClient.invalidateQueries({ queryKey: ['fetchIds'] }); // <-- refresh the gallery
120+
},
121+
}
122+
);
123+
};
124+
const handleDownload = () => {
125+
// TODO: Implement download logic for selectedImages
126+
};
127+
128+
const theme = useTheme();
129+
130+
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
131+
68132
return (
69133
<>
134+
{/* Selection Toolbar */}
135+
<Slide
136+
direction="down"
137+
in={selectedImages.length > 0}
138+
mountOnEnter
139+
unmountOnExit
140+
>
141+
<AppBar
142+
position="fixed"
143+
color="default"
144+
elevation={2}
145+
sx={{
146+
top: 0,
147+
left: 0,
148+
right: 0,
149+
background: theme.palette.background.paper,
150+
borderBottom: '1px solid #eee',
151+
zIndex: theme.zIndex.drawer + 2,
152+
}}
153+
>
154+
<Toolbar>
155+
<IconButton
156+
edge="start"
157+
color="inherit"
158+
onClick={handleClearSelection}
159+
>
160+
<CloseIcon />
161+
</IconButton>
162+
<Typography sx={{ flexGrow: 1, fontWeight: 600 }}>
163+
{selectedImages.length} selected
164+
</Typography>
165+
<Tooltip title="Download">
166+
<IconButton color="inherit" onClick={handleDownload}>
167+
<DownloadIcon />
168+
</IconButton>
169+
</Tooltip>
170+
<Tooltip title="Delete">
171+
<IconButton
172+
color="inherit"
173+
onClick={() => setOpenDeleteDialog(true)}
174+
>
175+
<DeleteIcon />
176+
</IconButton>
177+
</Tooltip>
178+
<Dialog
179+
open={openDeleteDialog}
180+
onClose={() => setOpenDeleteDialog(false)}
181+
aria-labelledby="delete-dialog-title"
182+
aria-describedby="delete-dialog-description"
183+
>
184+
<DialogTitle
185+
id="delete-dialog-title"
186+
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
187+
>
188+
<DeleteIcon color="error" sx={{ fontSize: 32 }} />
189+
Delete {selectedImages.length}{' '}
190+
{selectedImages.length === 1 ? 'item' : 'items'}?
191+
</DialogTitle>
192+
<Typography
193+
id="delete-dialog-description"
194+
sx={{ px: 3, pb: 1, color: 'text.secondary' }}
195+
>
196+
This action will move the selected{' '}
197+
{selectedImages.length === 1 ? 'item' : 'items'} to trash. You
198+
can restore them from trash later.
199+
</Typography>
200+
<DialogActions>
201+
<Button
202+
onClick={() => setOpenDeleteDialog(false)}
203+
variant="outlined"
204+
>
205+
Cancel
206+
</Button>
207+
<Button
208+
onClick={() => {
209+
handleDelete();
210+
setOpenDeleteDialog(false);
211+
}}
212+
color="error"
213+
variant="contained"
214+
>
215+
Move to Trash
216+
</Button>
217+
</DialogActions>
218+
</Dialog>
219+
</Toolbar>
220+
</AppBar>
221+
</Slide>
222+
223+
{selectedImages.length > 0 && <Toolbar />}
224+
70225
<Divider sx={{ mb: 2 }} />
71226
<Typography
72227
color="text.primary"
@@ -82,9 +237,35 @@ export const ImageGallery: React.FC = () => {
82237
.map((page) => page.properties)
83238
.flat()
84239
.map((thumbnail: IThumbnail, index) => (
85-
<Grid item xs={1} key={thumbnail.id}>
86-
<Paper elevation={0} onClick={() => openPreview(index)}>
87-
<ImageThumbnail id={thumbnail.id} mediaType={thumbnail.mediaType} />
240+
<Grid
241+
item
242+
xs={1}
243+
key={thumbnail.id}
244+
sx={{ position: 'relative' }}
245+
>
246+
<Paper elevation={0}>
247+
<SelectImageButton
248+
selected={selectedImages.includes(thumbnail.id)}
249+
onClick={(e) => {
250+
e.stopPropagation();
251+
toggleSelectImage(thumbnail.id);
252+
}}
253+
/>
254+
<div onClick={() => openPreview(index)}>
255+
{selectedImages.includes(thumbnail.id) ? (
256+
<Box component="section" sx={{ p: 1.5 }}>
257+
<ImageThumbnail
258+
id={thumbnail.id}
259+
mediaType={thumbnail.mediaType}
260+
/>
261+
</Box>
262+
) : (
263+
<ImageThumbnail
264+
id={thumbnail.id}
265+
mediaType={thumbnail.mediaType}
266+
/>
267+
)}
268+
</div>
88269
</Paper>
89270
</Grid>
90271
))}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
2+
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
3+
import IconButton from '@mui/material/IconButton';
4+
import React, { useState } from 'react';
5+
6+
interface SelectImageButtonProps {
7+
selected: boolean;
8+
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
9+
}
10+
11+
const SelectImageButton: React.FC<SelectImageButtonProps> = ({ selected, onClick }) => {
12+
const [hovered, setHovered] = useState(false);
13+
14+
return (
15+
<IconButton
16+
size="small"
17+
onClick={onClick}
18+
onMouseEnter={() => setHovered(true)}
19+
onMouseLeave={() => setHovered(false)}
20+
sx={{
21+
position: 'absolute',
22+
top: 10,
23+
left: 6,
24+
zIndex: 2,
25+
//transition: 'background 0.2s, box-shadow 0.2s, filter 0.2s',
26+
//boxShadow: 1,
27+
// '&:hover': {
28+
// background: 'rgba(26,115,232,0.15)',
29+
// },
30+
}}
31+
>
32+
{(() => {
33+
if (selected) {
34+
return (
35+
<CheckCircleIcon
36+
sx={{
37+
color: '#0B57D0',
38+
fontSize: 24,
39+
filter: 'drop-shadow(0 0 2px #fff)',
40+
}}
41+
/>
42+
);
43+
} else if (hovered) {
44+
return (
45+
<CheckCircleIcon
46+
sx={{
47+
color: 'white',
48+
fontSize: 24,
49+
}}
50+
/>
51+
);
52+
} else {
53+
return (
54+
<CheckCircleOutlineIcon
55+
sx={{
56+
color: 'gray',
57+
fontSize: 24,
58+
}}
59+
/>
60+
);
61+
}
62+
})()}
63+
</IconButton>
64+
);
65+
};
66+
67+
export default SelectImageButton;

0 commit comments

Comments
 (0)