feat: more functionality within files table
This commit is contained in:
parent
76845fc7e4
commit
e804d0b31e
8 changed files with 625 additions and 580 deletions
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
345
src/components/File/FileModal.tsx
Normal file
345
src/components/File/FileModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
90
src/components/File/index.tsx
Normal file
90
src/components/File/index.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
94
yarn.lock
94
yarn.lock
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue