feat: more functionality within files table

This commit is contained in:
dicedtomato 2023-02-27 01:52:22 +00:00 committed by GitHub
parent 76845fc7e4
commit e804d0b31e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 625 additions and 580 deletions

View file

@ -1,402 +0,0 @@
import {
ActionIcon,
Card,
Group,
LoadingOverlay,
Modal,
Select,
SimpleGrid,
Stack,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import useFetch from 'hooks/useFetch';
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
import { useFolders } from 'lib/queries/folders';
import { relativeTime } from 'lib/utils/client';
import { useState } from 'react';
import {
CalendarIcon,
ClockIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
DownloadIcon,
ExternalLinkIcon,
EyeIcon,
FileIcon,
FolderMinusIcon,
FolderPlusIcon,
HashIcon,
ImageIcon,
InfoIcon,
StarIcon,
} from './icons';
import MutedText from './MutedText';
import Type from './Type';
export function FileMeta({ Icon, title, subtitle, ...other }) {
return other.tooltip ? (
<Group>
<Icon size={24} />
<Tooltip label={other.tooltip}>
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Tooltip>
</Group>
) : (
<Group>
<Icon size={24} />
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Group>
);
}
export default function File({
image,
disableMediaPreview,
exifEnabled,
refreshImages,
reducedActions = false,
}) {
const [open, setOpen] = useState(false);
const [overrideRender, setOverrideRender] = useState(false);
const deleteFile = useFileDelete();
const favoriteFile = useFileFavorite();
const clipboard = useClipboard();
const folders = useFolders();
const loading = deleteFile.isLoading || favoriteFile.isLoading;
const handleDelete = async () => {
deleteFile.mutate(image.id, {
onSuccess: () => {
showNotification({
title: 'File Deleted',
message: '',
color: 'green',
icon: <DeleteIcon />,
});
},
onError: (res: any) => {
showNotification({
title: 'Failed to delete file',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
},
onSettled: () => {
setOpen(false);
},
});
};
const handleCopy = () => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
setOpen(false);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
});
};
const handleFavorite = async () => {
favoriteFile.mutate(
{ id: image.id, favorite: !image.favorite },
{
onSuccess: () => {
showNotification({
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
message: '',
icon: <StarIcon />,
});
},
onError: (res: any) => {
showNotification({
title: 'Failed to favorite file',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
},
}
);
};
const inFolder = image.folderId;
const refresh = () => {
refreshImages();
folders.refetch();
};
const removeFromFolder = async () => {
const res = await useFetch('/api/user/folders/' + image.folderId, 'DELETE', {
file: Number(image.id),
});
refresh();
if (!res.error) {
showNotification({
title: 'Removed from folder',
message: res.name,
color: 'green',
icon: <FolderMinusIcon />,
});
} else {
showNotification({
title: 'Failed to remove from folder',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
}
};
const addToFolder = async (t) => {
const res = await useFetch('/api/user/folders/' + t, 'POST', {
file: Number(image.id),
});
refresh();
if (!res.error) {
showNotification({
title: 'Added to folder',
message: res.name,
color: 'green',
icon: <FolderPlusIcon />,
});
} else {
showNotification({
title: 'Failed to add to folder',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
}
};
const createFolder = (t) => {
useFetch('/api/user/folders', 'POST', {
name: t,
add: [Number(image.id)],
}).then((res) => {
refresh();
if (!res.error) {
showNotification({
title: 'Created & added to folder',
message: res.name,
color: 'green',
icon: <FolderPlusIcon />,
});
} else {
showNotification({
title: 'Failed to create folder',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
}
});
return { value: t, label: t };
};
return (
<>
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{image.name}</Title>} size='xl'>
<LoadingOverlay visible={loading} />
<Stack>
<Type
file={image}
src={`/r/${encodeURI(image.name)}`}
alt={image.name}
popup
sx={{ minHeight: 200 }}
style={{ minHeight: 200 }}
disableMediaPreview={false}
overrideRender={overrideRender}
setOverrideRender={setOverrideRender}
/>
<SimpleGrid
my='md'
cols={3}
breakpoints={[
{ maxWidth: 600, cols: 1 },
{ maxWidth: 900, cols: 2 },
{ maxWidth: 1200, cols: 3 },
]}
>
<FileMeta Icon={FileIcon} title='Name' subtitle={image.name} />
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
<FileMeta Icon={EyeIcon} title='Views' subtitle={image?.views?.toLocaleString()} />
{image.maxViews && (
<FileMeta
Icon={EyeIcon}
title='Max views'
subtitle={image?.maxViews?.toLocaleString()}
tooltip={`This file will be deleted after being viewed ${image?.maxViews?.toLocaleString()} times.`}
/>
)}
<FileMeta
Icon={CalendarIcon}
title='Uploaded'
subtitle={relativeTime(new Date(image.createdAt))}
tooltip={new Date(image?.createdAt).toLocaleString()}
/>
{image.expiresAt && !reducedActions && (
<FileMeta
Icon={ClockIcon}
title='Expires'
subtitle={relativeTime(new Date(image.expiresAt))}
tooltip={new Date(image.expiresAt).toLocaleString()}
/>
)}
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
</SimpleGrid>
</Stack>
<Group position='apart' my='md'>
<Group position='left'>
{exifEnabled && !reducedActions && (
<Tooltip label='View Metadata'>
<ActionIcon
color='blue'
variant='filled'
onClick={() => window.open(`/dashboard/metadata/${image.id}`, '_blank')}
>
<InfoIcon />
</ActionIcon>
</Tooltip>
)}
{reducedActions ? null : inFolder && !folders.isLoading ? (
<Tooltip
label={`Remove from folder "${
folders.data.find((f) => f.id === image.folderId)?.name ?? ''
}"`}
>
<ActionIcon
color='red'
variant='filled'
onClick={removeFromFolder}
loading={folders.isLoading}
>
<FolderMinusIcon />
</ActionIcon>
</Tooltip>
) : (
<Tooltip label='Add to folder'>
<Select
onChange={addToFolder}
placeholder='Add to folder'
data={[
...(folders.data ? folders.data : []).map((folder) => ({
value: String(folder.id),
label: `${folder.id}: ${folder.name}`,
})),
]}
searchable
creatable
getCreateLabel={(query) => `Create folder "${query}"`}
onCreate={createFolder}
/>
</Tooltip>
)}
</Group>
<Group position='right'>
{reducedActions ? null : (
<>
<Tooltip label='Delete file'>
<ActionIcon color='red' variant='filled' onClick={handleDelete}>
<DeleteIcon />
</ActionIcon>
</Tooltip>
<Tooltip label={image.favorite ? 'Unfavorite' : 'Favorite'}>
<ActionIcon
color={image.favorite ? 'yellow' : 'gray'}
variant='filled'
onClick={handleFavorite}
>
<StarIcon />
</ActionIcon>
</Tooltip>
</>
)}
<Tooltip label='Open in new tab'>
<ActionIcon color='blue' variant='filled' onClick={() => window.open(image.url, '_blank')}>
<ExternalLinkIcon />
</ActionIcon>
</Tooltip>
<Tooltip label='Copy URL'>
<ActionIcon color='blue' variant='filled' onClick={handleCopy}>
<CopyIcon />
</ActionIcon>
</Tooltip>
<Tooltip label='Download'>
<ActionIcon
color='blue'
variant='filled'
onClick={() => window.open(`/r/${encodeURI(image.name)}?download=true`, '_blank')}
>
<DownloadIcon />
</ActionIcon>
</Tooltip>
</Group>
</Group>
</Modal>
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
<Card.Section>
<LoadingOverlay visible={loading} />
<Type
file={image}
sx={{
minHeight: 200,
maxHeight: 320,
fontSize: 70,
width: '100%',
cursor: 'pointer',
}}
style={{
minHeight: 200,
maxHeight: 320,
fontSize: 70,
width: '100%',
cursor: 'pointer',
}}
src={`/r/${encodeURI(image.name)}`}
alt={image.name}
onClick={() => setOpen(true)}
disableMediaPreview={disableMediaPreview}
/>
</Card.Section>
</Card>
</>
);
}

View file

@ -0,0 +1,345 @@
import {
ActionIcon,
Group,
LoadingOverlay,
Modal,
Select,
SimpleGrid,
Stack,
Title,
Tooltip,
} from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import useFetch from 'hooks/useFetch';
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
import { useFolders } from 'lib/queries/folders';
import { relativeTime } from 'lib/utils/client';
import { useState } from 'react';
import { FileMeta } from '.';
import {
CalendarIcon,
ClockIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
DownloadIcon,
ExternalLinkIcon,
EyeIcon,
FileIcon,
FolderMinusIcon,
FolderPlusIcon,
HashIcon,
ImageIcon,
InfoIcon,
StarIcon,
} from '../icons';
import Type from '../Type';
export default function FileModal({
open,
setOpen,
file,
loading,
refresh,
reducedActions = false,
exifEnabled,
}: {
open: boolean;
setOpen: (open: boolean) => void;
file: any;
loading: boolean;
refresh: () => void;
reducedActions?: boolean;
exifEnabled?: boolean;
}) {
const deleteFile = useFileDelete();
const favoriteFile = useFileFavorite();
const folders = useFolders();
const [overrideRender, setOverrideRender] = useState(false);
const clipboard = useClipboard();
const handleDelete = async () => {
deleteFile.mutate(file.id, {
onSuccess: () => {
showNotification({
title: 'File Deleted',
message: '',
color: 'green',
icon: <DeleteIcon />,
});
},
onError: (res: any) => {
showNotification({
title: 'Failed to delete file',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
},
onSettled: () => {
setOpen(false);
},
});
};
const handleCopy = () => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${file.url}`);
setOpen(false);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
});
};
const handleFavorite = async () => {
favoriteFile.mutate(
{ id: file.id, favorite: !file.favorite },
{
onSuccess: () => {
showNotification({
title: 'The file is now ' + (!file.favorite ? 'favorited' : 'unfavorited'),
message: '',
icon: <StarIcon />,
});
},
onError: (res: any) => {
showNotification({
title: 'Failed to favorite file',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
},
}
);
};
const inFolder = file.folderId;
const removeFromFolder = async () => {
const res = await useFetch('/api/user/folders/' + file.folderId, 'DELETE', {
file: Number(file.id),
});
refresh();
if (!res.error) {
showNotification({
title: 'Removed from folder',
message: res.name,
color: 'green',
icon: <FolderMinusIcon />,
});
} else {
showNotification({
title: 'Failed to remove from folder',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
}
};
const addToFolder = async (t) => {
const res = await useFetch('/api/user/folders/' + t, 'POST', {
file: Number(file.id),
});
refresh();
if (!res.error) {
showNotification({
title: 'Added to folder',
message: res.name,
color: 'green',
icon: <FolderPlusIcon />,
});
} else {
showNotification({
title: 'Failed to add to folder',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
}
};
const createFolder = (t) => {
useFetch('/api/user/folders', 'POST', {
name: t,
add: [Number(file.id)],
}).then((res) => {
refresh();
if (!res.error) {
showNotification({
title: 'Created & added to folder',
message: res.name,
color: 'green',
icon: <FolderPlusIcon />,
});
} else {
showNotification({
title: 'Failed to create folder',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
}
});
return { value: t, label: t };
};
return (
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{file.name}</Title>} size='xl'>
<LoadingOverlay visible={loading} />
<Stack>
<Type
file={file}
src={`/r/${encodeURI(file.name)}`}
alt={file.name}
popup
sx={{ minHeight: 200 }}
style={{ minHeight: 200 }}
disableMediaPreview={false}
overrideRender={overrideRender}
setOverrideRender={setOverrideRender}
/>
<SimpleGrid
my='md'
cols={3}
breakpoints={[
{ maxWidth: 600, cols: 1 },
{ maxWidth: 900, cols: 2 },
{ maxWidth: 1200, cols: 3 },
]}
>
<FileMeta Icon={FileIcon} title='Name' subtitle={file.name} />
<FileMeta Icon={ImageIcon} title='Type' subtitle={file.mimetype} />
<FileMeta Icon={EyeIcon} title='Views' subtitle={file?.views?.toLocaleString()} />
{file.maxViews && (
<FileMeta
Icon={EyeIcon}
title='Max views'
subtitle={file?.maxViews?.toLocaleString()}
tooltip={`This file will be deleted after being viewed ${file?.maxViews?.toLocaleString()} times.`}
/>
)}
<FileMeta
Icon={CalendarIcon}
title='Uploaded'
subtitle={relativeTime(new Date(file.createdAt))}
tooltip={new Date(file?.createdAt).toLocaleString()}
/>
{file.expiresAt && !reducedActions && (
<FileMeta
Icon={ClockIcon}
title='Expires'
subtitle={relativeTime(new Date(file.expiresAt))}
tooltip={new Date(file.expiresAt).toLocaleString()}
/>
)}
<FileMeta Icon={HashIcon} title='ID' subtitle={file.id} />
</SimpleGrid>
</Stack>
<Group position='apart' my='md'>
<Group position='left'>
{exifEnabled && !reducedActions && (
<Tooltip label='View Metadata'>
<ActionIcon
color='blue'
variant='filled'
onClick={() => window.open(`/dashboard/metadata/${file.id}`, '_blank')}
>
<InfoIcon />
</ActionIcon>
</Tooltip>
)}
{reducedActions ? null : inFolder && !folders.isLoading ? (
<Tooltip
label={`Remove from folder "${folders.data.find((f) => f.id === file.folderId)?.name ?? ''}"`}
>
<ActionIcon color='red' variant='filled' onClick={removeFromFolder} loading={folders.isLoading}>
<FolderMinusIcon />
</ActionIcon>
</Tooltip>
) : (
<Tooltip label='Add to folder'>
<Select
onChange={addToFolder}
placeholder='Add to folder'
data={[
...(folders.data ? folders.data : []).map((folder) => ({
value: String(folder.id),
label: `${folder.id}: ${folder.name}`,
})),
]}
searchable
creatable
getCreateLabel={(query) => `Create folder "${query}"`}
onCreate={createFolder}
/>
</Tooltip>
)}
</Group>
<Group position='right'>
{reducedActions ? null : (
<>
<Tooltip label='Delete file'>
<ActionIcon color='red' variant='filled' onClick={handleDelete}>
<DeleteIcon />
</ActionIcon>
</Tooltip>
<Tooltip label={file.favorite ? 'Unfavorite' : 'Favorite'}>
<ActionIcon
color={file.favorite ? 'yellow' : 'gray'}
variant='filled'
onClick={handleFavorite}
>
<StarIcon />
</ActionIcon>
</Tooltip>
</>
)}
<Tooltip label='Open in new tab'>
<ActionIcon color='blue' variant='filled' onClick={() => window.open(file.url, '_blank')}>
<ExternalLinkIcon />
</ActionIcon>
</Tooltip>
<Tooltip label='Copy URL'>
<ActionIcon color='blue' variant='filled' onClick={handleCopy}>
<CopyIcon />
</ActionIcon>
</Tooltip>
<Tooltip label='Download'>
<ActionIcon
color='blue'
variant='filled'
onClick={() => window.open(`/r/${encodeURI(file.name)}?download=true`, '_blank')}
>
<DownloadIcon />
</ActionIcon>
</Tooltip>
</Group>
</Group>
</Modal>
);
}

View file

@ -0,0 +1,90 @@
import { Card, Group, LoadingOverlay, Stack, Text, Tooltip } from '@mantine/core';
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
import { useFolders } from 'lib/queries/folders';
import { useState } from 'react';
import MutedText from '../MutedText';
import Type from '../Type';
import FileModal from './FileModal';
export function FileMeta({ Icon, title, subtitle, ...other }) {
return other.tooltip ? (
<Group>
<Icon size={24} />
<Tooltip label={other.tooltip}>
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Tooltip>
</Group>
) : (
<Group>
<Icon size={24} />
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Group>
);
}
export default function File({
image,
disableMediaPreview,
exifEnabled,
refreshImages,
reducedActions = false,
}) {
const [open, setOpen] = useState(false);
const deleteFile = useFileDelete();
const favoriteFile = useFileFavorite();
const loading = deleteFile.isLoading || favoriteFile.isLoading;
const folders = useFolders();
const refresh = () => {
refreshImages();
folders.refetch();
};
return (
<>
<FileModal
open={open}
setOpen={setOpen}
file={image}
loading={loading}
refresh={refresh}
reducedActions={reducedActions}
exifEnabled={exifEnabled}
/>
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
<Card.Section>
<LoadingOverlay visible={loading} />
<Type
file={image}
sx={{
minHeight: 200,
maxHeight: 320,
fontSize: 70,
width: '100%',
cursor: 'pointer',
}}
style={{
minHeight: 200,
maxHeight: 320,
fontSize: 70,
width: '100%',
cursor: 'pointer',
}}
src={`/r/${encodeURI(image.name)}`}
alt={image.name}
onClick={() => setOpen(true)}
disableMediaPreview={disableMediaPreview}
/>
</Card.Section>
</Card>
</>
);
}

View file

@ -1,16 +1,4 @@
import { import { Alert, Box, Button, Card, Center, Group, Image, LoadingOverlay, Text } from '@mantine/core';
Alert,
Box,
Button,
Card,
Center,
Container,
Group,
Image,
LoadingOverlay,
Text,
} from '@mantine/core';
import { Prism } from '@mantine/prism';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { AudioIcon, FileIcon, ImageIcon, PlayIcon } from './icons'; import { AudioIcon, FileIcon, ImageIcon, PlayIcon } from './icons';
import KaTeX from './render/KaTeX'; import KaTeX from './render/KaTeX';
@ -62,7 +50,7 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
}, []); }, []);
} }
const renderRenderAlert = () => { const renderAlert = () => {
return ( return (
<Alert color='blue' variant='outline' sx={{ width: '100%' }}> <Alert color='blue' variant='outline' sx={{ width: '100%' }}>
You are{props.overrideRender ? ' not ' : ' '}viewing a rendered version of the file You are{props.overrideRender ? ' not ' : ' '}viewing a rendered version of the file
@ -81,7 +69,7 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
if ((shouldRenderMarkdown || shouldRenderTex) && !props.overrideRender && popup) if ((shouldRenderMarkdown || shouldRenderTex) && !props.overrideRender && popup)
return ( return (
<> <>
{renderRenderAlert()} {renderAlert()}
<Card p='md' my='sm'> <Card p='md' my='sm'>
{shouldRenderMarkdown && <Markdown code={text} />} {shouldRenderMarkdown && <Markdown code={text} />}
{shouldRenderTex && <KaTeX code={text} />} {shouldRenderTex && <KaTeX code={text} />}
@ -90,7 +78,7 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
); );
if (media && disableMediaPreview) { if (media && disableMediaPreview) {
return <Placeholder Icon={FileIcon} text={`Click to view file (${name})`} {...props} />; return <Placeholder Icon={FileIcon} text={`Click to view file (${file.name})`} {...props} />;
} }
return popup ? ( return popup ? (
@ -110,7 +98,7 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
<LoadingOverlay visible={loading} /> <LoadingOverlay visible={loading} />
) : ( ) : (
<> <>
{(shouldRenderMarkdown || shouldRenderTex) && renderRenderAlert()} {(shouldRenderMarkdown || shouldRenderTex) && renderAlert()}
<PrismCode code={text} ext={file.name.split('.').pop()} {...props} /> <PrismCode code={text} ext={file.name.split('.').pop()} {...props} />
</> </>
)} )}

View file

@ -1,42 +1,88 @@
import { DataGrid, dateFilterFn, stringFilterFn } from '@dicedtomato/mantine-data-grid'; import { ActionIcon, Box, Group, Title, Tooltip } from '@mantine/core';
import { Title, useMantineTheme, Box } from '@mantine/core';
import { useClipboard } from '@mantine/hooks'; import { useClipboard } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { CopyIcon, CrossIcon, DeleteIcon, EnterIcon } from 'components/icons'; import FileModal from 'components/File/FileModal';
import { CopyIcon, CrossIcon, DeleteIcon, EnterIcon, FileIcon } from 'components/icons';
import Link from 'components/Link'; import Link from 'components/Link';
import MutedText from 'components/MutedText'; import MutedText from 'components/MutedText';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import { useFiles, useRecent } from 'lib/queries/files'; import { usePaginatedFiles, useRecent } from 'lib/queries/files';
import { useStats } from 'lib/queries/stats'; import { useStats } from 'lib/queries/stats';
import { userSelector } from 'lib/recoil/user'; import { userSelector } from 'lib/recoil/user';
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import RecentFiles from './RecentFiles'; import RecentFiles from './RecentFiles';
import { StatCards } from './StatCards'; import { StatCards } from './StatCards';
export default function Dashboard({ disableMediaPreview, exifEnabled }) { export default function Dashboard({ disableMediaPreview, exifEnabled }) {
const user = useRecoilValue(userSelector); const user = useRecoilValue(userSelector);
const theme = useMantineTheme();
const images = useFiles();
const recent = useRecent('media'); const recent = useRecent('media');
const stats = useStats(); const stats = useStats();
const clipboard = useClipboard(); const clipboard = useClipboard();
const updateImages = () => { // pagination
images.refetch(); const [, setNumPages] = useState(0);
const [page, setPage] = useState(1);
const [numFiles, setNumFiles] = useState(0);
useEffect(() => {
(async () => {
const { count } = await useFetch('/api/user/paged?count=true');
setNumPages(count);
const { count: filesCount } = await useFetch('/api/user/files?count=true');
setNumFiles(filesCount);
})();
}, [page]);
const files = usePaginatedFiles(page);
// sorting
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
columnAccessor: 'date',
direction: 'asc',
});
const [records, setRecords] = useState(files.data);
useEffect(() => {
setRecords(files.data);
}, [files.data]);
useEffect(() => {
if (!records || records.length === 0) return;
const sortedRecords = [...records].sort((a, b) => {
if (sortStatus.direction === 'asc') {
return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1;
}
return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1;
});
setRecords(sortedRecords);
}, [sortStatus]);
// file modal on click
const [open, setOpen] = useState(false);
const [selectedFile, setSelectedFile] = useState(null);
const updateFiles = () => {
files.refetch();
recent.refetch(); recent.refetch();
stats.refetch(); stats.refetch();
}; };
const deleteImage = async ({ original }) => { const deleteFile = async (file) => {
const res = await useFetch('/api/user/files', 'DELETE', { const res = await useFetch('/api/user/files', 'DELETE', {
id: original.id, id: file.id,
}); });
if (!res.error) { if (!res.error) {
updateImages(); updateFiles();
showNotification({ showNotification({
title: 'File Deleted', title: 'File Deleted',
message: `${original.name}`, message: `${file.name}`,
color: 'green', color: 'green',
icon: <DeleteIcon />, icon: <DeleteIcon />,
}); });
@ -50,8 +96,8 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) {
} }
}; };
const copyImage = async ({ original }) => { const copyFile = async (file) => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${original.url}`); clipboard.copy(`${window.location.protocol}//${window.location.host}${file.url}`);
if (!navigator.clipboard) if (!navigator.clipboard)
showNotification({ showNotification({
title: 'Unable to copy to clipboard', title: 'Unable to copy to clipboard',
@ -63,22 +109,34 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) {
title: 'Copied to clipboard', title: 'Copied to clipboard',
message: ( message: (
<a <a
href={`${window.location.protocol}//${window.location.host}${original.url}`} href={`${window.location.protocol}//${window.location.host}${file.url}`}
>{`${window.location.protocol}//${window.location.host}${original.url}`}</a> >{`${window.location.protocol}//${window.location.host}${file.url}`}</a>
), ),
icon: <CopyIcon />, icon: <CopyIcon />,
}); });
}; };
const viewImage = async ({ original }) => { const viewFile = async (file) => {
window.open(`${window.location.protocol}//${window.location.host}${original.url}`); window.open(`${window.location.protocol}//${window.location.host}${file.url}`);
}; };
return ( return (
<div> <div>
{selectedFile && (
<FileModal
open={open}
setOpen={setOpen}
file={selectedFile}
loading={files.isLoading}
refresh={() => files.refetch()}
reducedActions={false}
exifEnabled={exifEnabled}
/>
)}
<Title>Welcome back, {user?.username}</Title> <Title>Welcome back, {user?.username}</Title>
<MutedText size='md'> <MutedText size='md'>
You have <b>{images.isSuccess ? images.data.length : '...'}</b> files You have <b>{numFiles === 0 ? '...' : numFiles}</b> files
</MutedText> </MutedText>
<StatCards /> <StatCards />
@ -90,74 +148,92 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) {
<MutedText size='md'> <MutedText size='md'>
View your gallery <Link href='/dashboard/files'>here</Link>. View your gallery <Link href='/dashboard/files'>here</Link>.
</MutedText> </MutedText>
<DataGrid
data={images.data ?? []} <DataTable
loading={images.isLoading} withBorder
withPagination={true} borderRadius='md'
withColumnResizing={false} highlightOnHover
withColumnFilters={true} verticalSpacing='sm'
noEllipsis={true}
withSorting={true}
highlightOnHover={true}
CopyIcon={CopyIcon}
DeleteIcon={DeleteIcon}
EnterIcon={EnterIcon}
deleteImage={deleteImage}
copyImage={copyImage}
viewImage={viewImage}
styles={{
dataCell: {
width: '100%',
},
td: {
':nth-of-child(1)': {
minWidth: 170,
},
':nth-of-child(2)': {
minWidth: 100,
},
},
th: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0],
':nth-of-child(1)': {
minWidth: 170,
padding: theme.spacing.lg,
borderTopLeftRadius: theme.radius.sm,
},
':nth-of-child(2)': {
minWidth: 100,
padding: theme.spacing.lg,
},
':nth-of-child(3)': {
padding: theme.spacing.lg,
},
':nth-of-child(4)': {
padding: theme.spacing.lg,
borderTopRightRadius: theme.radius.sm,
},
},
thead: {
backgroundColor: theme.colors.dark[6],
},
}}
empty={<></>}
columns={[ columns={[
{ accessor: 'name', sortable: true },
{ accessor: 'mimetype', sortable: true },
{ {
accessorKey: 'name', accessor: 'createdAt',
header: 'Name', sortable: true,
filterFn: stringFilterFn, render: (file) => new Date(file.createdAt).toLocaleString(),
}, },
{ {
accessorKey: 'mimetype', accessor: 'actions',
header: 'Type', textAlignment: 'right',
filterFn: stringFilterFn, render: (file) => (
}, <Group spacing={4} position='right' noWrap>
{ <Tooltip label='More details'>
accessorKey: 'createdAt', <ActionIcon
header: 'Date', onClick={() => {
filterFn: dateFilterFn, setSelectedFile(file);
setOpen(true);
}}
color='blue'
>
<FileIcon />
</ActionIcon>
</Tooltip>
<Tooltip label='Open file in new tab'>
<ActionIcon onClick={() => viewFile(file)} color='blue'>
<EnterIcon />
</ActionIcon>
</Tooltip>
<ActionIcon onClick={() => copyFile(file)} color='green'>
<CopyIcon />
</ActionIcon>
<ActionIcon onClick={() => deleteFile(file)} color='red'>
<DeleteIcon />
</ActionIcon>
</Group>
),
}, },
]} ]}
records={records ?? []}
fetching={files.isLoading}
loaderBackgroundBlur={5}
loaderVariant='dots'
minHeight={620}
page={page}
onPageChange={setPage}
recordsPerPage={16}
totalRecords={numFiles}
sortStatus={sortStatus}
onSortStatusChange={setSortStatus}
rowContextMenu={{
shadow: 'xl',
borderRadius: 'md',
items: (file) => [
{
key: 'view',
icon: <EnterIcon />,
title: `View ${file.name}`,
onClick: () => viewFile(file),
},
{
key: 'copy',
icon: <CopyIcon />,
title: `Copy ${file.name}`,
onClick: () => copyFile(file),
},
{
key: 'delete',
icon: <DeleteIcon />,
title: `Delete ${file.name}`,
onClick: () => deleteFile(file),
},
],
}}
onCellClick={({ record: file }) => {
setSelectedFile(file);
setOpen(true);
}}
/> />
</Box> </Box>
</div> </div>

View file

@ -60,6 +60,16 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
delete image.password; delete image.password;
return res.json(image); return res.json(image);
} else { } else {
if (req.query.count) {
const count = await prisma.file.count({
where: {
userId: user.id,
favorite: !!req.query.favorite,
},
});
return res.json({ count });
}
let files: { let files: {
favorite: boolean; favorite: boolean;
createdAt: Date; createdAt: Date;

View file

@ -27,12 +27,8 @@
}, },
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",
"zip-env.d.ts",
"**/*.ts", "**/*.ts",
"**/*.tsx", "**/*.tsx",
"**/**/*.ts",
"**/**/*.tsx",
"prisma/seed.ts"
], ],
"exclude": ["node_modules", "dist", ".yarn", ".next"] "exclude": ["node_modules", "dist", ".yarn", ".next"]
} }

View file

@ -1167,33 +1167,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@dicedtomato/mantine-data-grid@npm:0.0.23":
version: 0.0.23
resolution: "@dicedtomato/mantine-data-grid@npm:0.0.23"
dependencies:
"@emotion/react": ^11.10.4
"@mantine/core": ^5.5.6
"@mantine/dates": ^5.5.6
"@mantine/hooks": ^5.5.6
"@tanstack/react-table": ^8.5.15
dayjs: ^1.11.4
react: ^18.0.0
react-dom: ^18.0.0
tabler-icons-react: ^1.54.0
peerDependencies:
"@mantine/core": ^5.0.0
"@mantine/dates": ^5.0.0
"@mantine/hooks": ^5.0.0
dayjs: ^1.11.4
react: ^18.0.0
react-dom: ^18.0.0
peerDependenciesMeta:
dayjs:
optional: true
checksum: e08bfff49f4ef58e88169fe31c1589d9e94ec5b94a8e04d39e0ddbd833ae79fe3a67350e7442d8d15ec0e3f887dd872af88eacabfce71fe2f3bd6b280811bdf3
languageName: node
linkType: hard
"@emotion/babel-plugin@npm:^11.10.5": "@emotion/babel-plugin@npm:^11.10.5":
version: 11.10.5 version: 11.10.5
resolution: "@emotion/babel-plugin@npm:11.10.5" resolution: "@emotion/babel-plugin@npm:11.10.5"
@ -1243,7 +1216,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@emotion/react@npm:^11.10.4, @emotion/react@npm:^11.10.5": "@emotion/react@npm:^11.10.5":
version: 11.10.5 version: 11.10.5
resolution: "@emotion/react@npm:11.10.5" resolution: "@emotion/react@npm:11.10.5"
dependencies: dependencies:
@ -1479,7 +1452,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/core@npm:^5.5.6, @mantine/core@npm:^5.9.2": "@mantine/core@npm:^5.9.2":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/core@npm:5.9.3" resolution: "@mantine/core@npm:5.9.3"
dependencies: dependencies:
@ -1496,20 +1469,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/dates@npm:^5.5.6":
version: 5.9.3
resolution: "@mantine/dates@npm:5.9.3"
dependencies:
"@mantine/utils": 5.9.3
peerDependencies:
"@mantine/core": 5.9.3
"@mantine/hooks": 5.9.3
dayjs: ">=1.0.0"
react: ">=16.8.0"
checksum: fc7c8d19ab10c1d997a882debae74f21fa3a2a59ee834b901713e5ddd9feba7197f61fb68a9a27794071d83d7690564fc45793a6966eebf5f55dff368f837aee
languageName: node
linkType: hard
"@mantine/dropzone@npm:^5.9.2": "@mantine/dropzone@npm:^5.9.2":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/dropzone@npm:5.9.3" resolution: "@mantine/dropzone@npm:5.9.3"
@ -1537,7 +1496,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/hooks@npm:^5.5.6, @mantine/hooks@npm:^5.9.2": "@mantine/hooks@npm:^5.9.2":
version: 5.9.3 version: 5.9.3
resolution: "@mantine/hooks@npm:5.9.3" resolution: "@mantine/hooks@npm:5.9.3"
peerDependencies: peerDependencies:
@ -2356,25 +2315,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@tanstack/react-table@npm:^8.5.15":
version: 8.7.0
resolution: "@tanstack/react-table@npm:8.7.0"
dependencies:
"@tanstack/table-core": 8.7.0
peerDependencies:
react: ">=16"
react-dom: ">=16"
checksum: 5f739f619dacfd134449bc8cff819d7e1b9549b8c8b20c293f2c786098a53d20fe5755727d8abb6fa56a64583792bbc89d8b1c7cd24ac3497495d2c20bac9e77
languageName: node
linkType: hard
"@tanstack/table-core@npm:8.7.0":
version: 8.7.0
resolution: "@tanstack/table-core@npm:8.7.0"
checksum: 835511727ab5651066a4cbc98916fbc2463ec6d6ddc5c74c1fec5713eeba8e26641653c1a35b69b303f0c58f574416a09e26da44cf7e9a884cb954fc26b12afd
languageName: node
linkType: hard
"@tediousjs/connection-string@npm:^0.4.1": "@tediousjs/connection-string@npm:^0.4.1":
version: 0.4.1 version: 0.4.1
resolution: "@tediousjs/connection-string@npm:0.4.1" resolution: "@tediousjs/connection-string@npm:0.4.1"
@ -4193,7 +4133,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dayjs@npm:^1.11.4, dayjs@npm:^1.11.7": "dayjs@npm:^1.11.7":
version: 1.11.7 version: 1.11.7
resolution: "dayjs@npm:1.11.7" resolution: "dayjs@npm:1.11.7"
checksum: 5003a7c1dd9ed51385beb658231c3548700b82d3548c0cfbe549d85f2d08e90e972510282b7506941452c58d32136d6362f009c77ca55381a09c704e9f177ebb checksum: 5003a7c1dd9ed51385beb658231c3548700b82d3548c0cfbe549d85f2d08e90e972510282b7506941452c58d32136d6362f009c77ca55381a09c704e9f177ebb
@ -7188,6 +7128,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mantine-datatable@npm:^1.8.6":
version: 1.8.6
resolution: "mantine-datatable@npm:1.8.6"
peerDependencies:
"@mantine/core": ^5.10.4
"@mantine/hooks": ^5.10.4
react: ^18.2.0
checksum: a4558418067ada3e269e3d9dc0153b613b031ecd5fccd484b45a2b13e403084971870515d51c888d933eb2f4d85f6df2ad221b0c84a2bf2cd602d27938bb9029
languageName: node
linkType: hard
"mariadb@npm:3.0.1": "mariadb@npm:3.0.1":
version: 3.0.1 version: 3.0.1
resolution: "mariadb@npm:3.0.1" resolution: "mariadb@npm:3.0.1"
@ -9291,7 +9242,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-dom@npm:^18.0.0, react-dom@npm:^18.2.0": "react-dom@npm:^18.2.0":
version: 18.2.0 version: 18.2.0
resolution: "react-dom@npm:18.2.0" resolution: "react-dom@npm:18.2.0"
dependencies: dependencies:
@ -9450,7 +9401,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react@npm:^18.0.0, react@npm:^18.2.0": "react@npm:^18.2.0":
version: 18.2.0 version: 18.2.0
resolution: "react@npm:18.2.0" resolution: "react@npm:18.2.0"
dependencies: dependencies:
@ -10571,15 +10522,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tabler-icons-react@npm:^1.54.0":
version: 1.56.0
resolution: "tabler-icons-react@npm:1.56.0"
peerDependencies:
react: ">= 16.8.0"
checksum: 0b058055826fe478afa5c72641bb5207b6332b177d6ed7f505ebaad632cf33a63f0a3bf87af103af2b65bb0fc7b76f056ef03328fea7fa0c7378e30a82c3212b
languageName: node
linkType: hard
"tapable@npm:^2.2.0": "tapable@npm:^2.2.0":
version: 2.2.1 version: 2.2.1
resolution: "tapable@npm:2.2.1" resolution: "tapable@npm:2.2.1"
@ -11605,7 +11547,6 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "zipline@workspace:." resolution: "zipline@workspace:."
dependencies: dependencies:
"@dicedtomato/mantine-data-grid": 0.0.23
"@emotion/react": ^11.10.5 "@emotion/react": ^11.10.5
"@emotion/server": ^11.10.0 "@emotion/server": ^11.10.0
"@mantine/core": ^5.9.2 "@mantine/core": ^5.9.2
@ -11647,6 +11588,7 @@ __metadata:
fflate: ^0.7.4 fflate: ^0.7.4
find-my-way: ^7.3.1 find-my-way: ^7.3.1
katex: ^0.16.4 katex: ^0.16.4
mantine-datatable: ^1.8.6
minio: ^7.0.32 minio: ^7.0.32
ms: canary ms: canary
multer: ^1.4.5-lts.1 multer: ^1.4.5-lts.1