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 {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Container,
|
||||
Group,
|
||||
Image,
|
||||
LoadingOverlay,
|
||||
Text,
|
||||
} from '@mantine/core';
|
||||
import { Prism } from '@mantine/prism';
|
||||
import { Alert, Box, Button, Card, Center, Group, Image, LoadingOverlay, Text } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AudioIcon, FileIcon, ImageIcon, PlayIcon } from './icons';
|
||||
import KaTeX from './render/KaTeX';
|
||||
|
@ -62,7 +50,7 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
|||
}, []);
|
||||
}
|
||||
|
||||
const renderRenderAlert = () => {
|
||||
const renderAlert = () => {
|
||||
return (
|
||||
<Alert color='blue' variant='outline' sx={{ width: '100%' }}>
|
||||
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)
|
||||
return (
|
||||
<>
|
||||
{renderRenderAlert()}
|
||||
{renderAlert()}
|
||||
<Card p='md' my='sm'>
|
||||
{shouldRenderMarkdown && <Markdown code={text} />}
|
||||
{shouldRenderTex && <KaTeX code={text} />}
|
||||
|
@ -90,7 +78,7 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
|||
);
|
||||
|
||||
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 ? (
|
||||
|
@ -110,7 +98,7 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
|||
<LoadingOverlay visible={loading} />
|
||||
) : (
|
||||
<>
|
||||
{(shouldRenderMarkdown || shouldRenderTex) && renderRenderAlert()}
|
||||
{(shouldRenderMarkdown || shouldRenderTex) && renderAlert()}
|
||||
<PrismCode code={text} ext={file.name.split('.').pop()} {...props} />
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -1,42 +1,88 @@
|
|||
import { DataGrid, dateFilterFn, stringFilterFn } from '@dicedtomato/mantine-data-grid';
|
||||
import { Title, useMantineTheme, Box } from '@mantine/core';
|
||||
import { ActionIcon, Box, Group, Title, Tooltip } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
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 MutedText from 'components/MutedText';
|
||||
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 { userSelector } from 'lib/recoil/user';
|
||||
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import RecentFiles from './RecentFiles';
|
||||
import { StatCards } from './StatCards';
|
||||
|
||||
export default function Dashboard({ disableMediaPreview, exifEnabled }) {
|
||||
const user = useRecoilValue(userSelector);
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const images = useFiles();
|
||||
const recent = useRecent('media');
|
||||
const stats = useStats();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const updateImages = () => {
|
||||
images.refetch();
|
||||
// pagination
|
||||
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();
|
||||
stats.refetch();
|
||||
};
|
||||
|
||||
const deleteImage = async ({ original }) => {
|
||||
const deleteFile = async (file) => {
|
||||
const res = await useFetch('/api/user/files', 'DELETE', {
|
||||
id: original.id,
|
||||
id: file.id,
|
||||
});
|
||||
if (!res.error) {
|
||||
updateImages();
|
||||
updateFiles();
|
||||
showNotification({
|
||||
title: 'File Deleted',
|
||||
message: `${original.name}`,
|
||||
message: `${file.name}`,
|
||||
color: 'green',
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
|
@ -50,8 +96,8 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) {
|
|||
}
|
||||
};
|
||||
|
||||
const copyImage = async ({ original }) => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${original.url}`);
|
||||
const copyFile = async (file) => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${file.url}`);
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
|
@ -63,22 +109,34 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) {
|
|||
title: 'Copied to clipboard',
|
||||
message: (
|
||||
<a
|
||||
href={`${window.location.protocol}//${window.location.host}${original.url}`}
|
||||
>{`${window.location.protocol}//${window.location.host}${original.url}`}</a>
|
||||
href={`${window.location.protocol}//${window.location.host}${file.url}`}
|
||||
>{`${window.location.protocol}//${window.location.host}${file.url}`}</a>
|
||||
),
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const viewImage = async ({ original }) => {
|
||||
window.open(`${window.location.protocol}//${window.location.host}${original.url}`);
|
||||
const viewFile = async (file) => {
|
||||
window.open(`${window.location.protocol}//${window.location.host}${file.url}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
<MutedText size='md'>
|
||||
You have <b>{images.isSuccess ? images.data.length : '...'}</b> files
|
||||
You have <b>{numFiles === 0 ? '...' : numFiles}</b> files
|
||||
</MutedText>
|
||||
|
||||
<StatCards />
|
||||
|
@ -90,74 +148,92 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) {
|
|||
<MutedText size='md'>
|
||||
View your gallery <Link href='/dashboard/files'>here</Link>.
|
||||
</MutedText>
|
||||
<DataGrid
|
||||
data={images.data ?? []}
|
||||
loading={images.isLoading}
|
||||
withPagination={true}
|
||||
withColumnResizing={false}
|
||||
withColumnFilters={true}
|
||||
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={<></>}
|
||||
|
||||
<DataTable
|
||||
withBorder
|
||||
borderRadius='md'
|
||||
highlightOnHover
|
||||
verticalSpacing='sm'
|
||||
columns={[
|
||||
{ accessor: 'name', sortable: true },
|
||||
{ accessor: 'mimetype', sortable: true },
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
filterFn: stringFilterFn,
|
||||
accessor: 'createdAt',
|
||||
sortable: true,
|
||||
render: (file) => new Date(file.createdAt).toLocaleString(),
|
||||
},
|
||||
{
|
||||
accessorKey: 'mimetype',
|
||||
header: 'Type',
|
||||
filterFn: stringFilterFn,
|
||||
},
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: 'Date',
|
||||
filterFn: dateFilterFn,
|
||||
accessor: 'actions',
|
||||
textAlignment: 'right',
|
||||
render: (file) => (
|
||||
<Group spacing={4} position='right' noWrap>
|
||||
<Tooltip label='More details'>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
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>
|
||||
</div>
|
||||
|
|
|
@ -60,6 +60,16 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
delete image.password;
|
||||
return res.json(image);
|
||||
} 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: {
|
||||
favorite: boolean;
|
||||
createdAt: Date;
|
||||
|
|
|
@ -27,12 +27,8 @@
|
|||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"zip-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/**/*.ts",
|
||||
"**/**/*.tsx",
|
||||
"prisma/seed.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "dist", ".yarn", ".next"]
|
||||
}
|
||||
|
|
94
yarn.lock
94
yarn.lock
|
@ -1167,33 +1167,6 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 11.10.5
|
||||
resolution: "@emotion/babel-plugin@npm:11.10.5"
|
||||
|
@ -1243,7 +1216,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emotion/react@npm:^11.10.4, @emotion/react@npm:^11.10.5":
|
||||
"@emotion/react@npm:^11.10.5":
|
||||
version: 11.10.5
|
||||
resolution: "@emotion/react@npm:11.10.5"
|
||||
dependencies:
|
||||
|
@ -1479,7 +1452,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/core@npm:^5.5.6, @mantine/core@npm:^5.9.2":
|
||||
"@mantine/core@npm:^5.9.2":
|
||||
version: 5.9.3
|
||||
resolution: "@mantine/core@npm:5.9.3"
|
||||
dependencies:
|
||||
|
@ -1496,20 +1469,6 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 5.9.3
|
||||
resolution: "@mantine/dropzone@npm:5.9.3"
|
||||
|
@ -1537,7 +1496,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/hooks@npm:^5.5.6, @mantine/hooks@npm:^5.9.2":
|
||||
"@mantine/hooks@npm:^5.9.2":
|
||||
version: 5.9.3
|
||||
resolution: "@mantine/hooks@npm:5.9.3"
|
||||
peerDependencies:
|
||||
|
@ -2356,25 +2315,6 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 0.4.1
|
||||
resolution: "@tediousjs/connection-string@npm:0.4.1"
|
||||
|
@ -4193,7 +4133,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dayjs@npm:^1.11.4, dayjs@npm:^1.11.7":
|
||||
"dayjs@npm:^1.11.7":
|
||||
version: 1.11.7
|
||||
resolution: "dayjs@npm:1.11.7"
|
||||
checksum: 5003a7c1dd9ed51385beb658231c3548700b82d3548c0cfbe549d85f2d08e90e972510282b7506941452c58d32136d6362f009c77ca55381a09c704e9f177ebb
|
||||
|
@ -7188,6 +7128,17 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 3.0.1
|
||||
resolution: "mariadb@npm:3.0.1"
|
||||
|
@ -9291,7 +9242,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dom@npm:^18.0.0, react-dom@npm:^18.2.0":
|
||||
"react-dom@npm:^18.2.0":
|
||||
version: 18.2.0
|
||||
resolution: "react-dom@npm:18.2.0"
|
||||
dependencies:
|
||||
|
@ -9450,7 +9401,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react@npm:^18.0.0, react@npm:^18.2.0":
|
||||
"react@npm:^18.2.0":
|
||||
version: 18.2.0
|
||||
resolution: "react@npm:18.2.0"
|
||||
dependencies:
|
||||
|
@ -10571,15 +10522,6 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.2.1
|
||||
resolution: "tapable@npm:2.2.1"
|
||||
|
@ -11605,7 +11547,6 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "zipline@workspace:."
|
||||
dependencies:
|
||||
"@dicedtomato/mantine-data-grid": 0.0.23
|
||||
"@emotion/react": ^11.10.5
|
||||
"@emotion/server": ^11.10.0
|
||||
"@mantine/core": ^5.9.2
|
||||
|
@ -11647,6 +11588,7 @@ __metadata:
|
|||
fflate: ^0.7.4
|
||||
find-my-way: ^7.3.1
|
||||
katex: ^0.16.4
|
||||
mantine-datatable: ^1.8.6
|
||||
minio: ^7.0.32
|
||||
ms: canary
|
||||
multer: ^1.4.5-lts.1
|
||||
|
|
Loading…
Reference in a new issue