feat: revamp file gallery
This commit is contained in:
parent
b7560c80aa
commit
56ff86db44
15 changed files with 171 additions and 122 deletions
|
@ -1,12 +1,36 @@
|
|||
{
|
||||
"extends": ["next", "next/core-web-vitals"],
|
||||
"extends": [
|
||||
"next",
|
||||
"next/core-web-vitals"
|
||||
],
|
||||
"rules": {
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"jsx-quotes": ["error", "prefer-single"],
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"jsx-quotes": [
|
||||
"error",
|
||||
"prefer-single"
|
||||
],
|
||||
"react/prop-types": "off",
|
||||
"react-hooks/rules-of-hooks": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
|
@ -20,6 +44,8 @@
|
|||
"react/react-in-jsx-scope": "off",
|
||||
"react/require-render-return": "error",
|
||||
"react/style-prop-object": "warn",
|
||||
"@next/next/no-img-element": "off"
|
||||
"@next/next/no-img-element": "off",
|
||||
"jsx-a11y/alt-text": "off",
|
||||
"react/display-name": "off"
|
||||
}
|
||||
}
|
|
@ -1,35 +1,49 @@
|
|||
import { Button, Card, Group, Image as MImage, Modal, Title } from '@mantine/core';
|
||||
import { Button, Card, Grid, Group, Image as MImage, Modal, Stack, Text, Title, useMantineTheme } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useState } from 'react';
|
||||
import { CopyIcon, CrossIcon, DeleteIcon, StarIcon } from './icons';
|
||||
import Type from './Type';
|
||||
import { CalendarIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HashIcon, ImageIcon, StarIcon } from './icons';
|
||||
import MutedText from './MutedText';
|
||||
|
||||
export function FileMeta({ Icon, title, subtitle }) {
|
||||
return (
|
||||
<Group>
|
||||
<Icon size={24} />
|
||||
<Stack spacing={1}>
|
||||
<Text>{title}</Text>
|
||||
<MutedText size='md'>{subtitle}</MutedText>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function File({ image, updateImages }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [t] = useState(image.mimetype.split('/')[0]);
|
||||
const notif = useNotifications();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
|
||||
if (!res.error) {
|
||||
updateImages(true);
|
||||
notif.showNotification({
|
||||
title: 'Image Deleted',
|
||||
title: 'File Deleted',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
} else {
|
||||
notif.showNotification({
|
||||
title: 'Failed to delete image',
|
||||
title: 'Failed to delete file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
|
@ -53,13 +67,6 @@ export default function File({ image, updateImages }) {
|
|||
});
|
||||
};
|
||||
|
||||
const Type = (props) => {
|
||||
return {
|
||||
'video': <video controls {...props} />,
|
||||
'image': <MImage withPlaceholder {...props} />,
|
||||
'audio': <audio controls {...props} />,
|
||||
}[t];
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -67,11 +74,27 @@ export default function File({ image, updateImages }) {
|
|||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title={<Title>{image.file}</Title>}
|
||||
size='xl'
|
||||
overlayBlur={3}
|
||||
overlayColor={theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white'}
|
||||
>
|
||||
<Type
|
||||
src={image.url}
|
||||
alt={image.file}
|
||||
/>
|
||||
<Stack>
|
||||
<Type
|
||||
file={image}
|
||||
src={image.url}
|
||||
alt={image.file}
|
||||
popup
|
||||
sx={{ minHeight: 200 }}
|
||||
style={{ minHeight: 200 }}
|
||||
/>
|
||||
<Stack>
|
||||
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
|
||||
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
|
||||
<FileMeta Icon={CalendarIcon} title='Uploaded at' subtitle={new Date(image.created_at).toLocaleString()} />
|
||||
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Group position='right' mt={22}>
|
||||
<Button onClick={handleCopy}>Copy</Button>
|
||||
<Button onClick={handleDelete}>Delete</Button>
|
||||
|
@ -81,8 +104,9 @@ export default function File({ image, updateImages }) {
|
|||
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
|
||||
<Card.Section>
|
||||
<Type
|
||||
sx={{ maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }}
|
||||
style={{ maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }}
|
||||
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={image.url}
|
||||
alt={image.file}
|
||||
onClick={() => setOpen(true)}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
/* eslint-disable jsx-a11y/alt-text */
|
||||
/* eslint-disable react/jsx-key */
|
||||
/* eslint-disable react/display-name */
|
||||
// Code taken from https://codesandbox.io/s/eojw8 and is modified a bit (the entire src/components/table directory)
|
||||
import {
|
||||
ActionIcon,
|
||||
|
@ -86,17 +84,14 @@ export default function ImagesTable({
|
|||
|
||||
const getPageRecordInfo = () => {
|
||||
const firstRowNum = pageIndex * pageSize + 1;
|
||||
const totalRows = serverSideDataSource ? total : rows.length;
|
||||
const totalRows = rows.length;
|
||||
|
||||
const currLastRowNum = (pageIndex + 1) * pageSize;
|
||||
let lastRowNum = currLastRowNum < totalRows ? currLastRowNum : totalRows;
|
||||
return `${firstRowNum} - ${lastRowNum} of ${totalRows}`;
|
||||
};
|
||||
|
||||
const getPageCount = () => {
|
||||
const totalRows = serverSideDataSource ? total : rows.length;
|
||||
return Math.ceil(totalRows / pageSize);
|
||||
};
|
||||
const getPageCount = () => Math.ceil(rows.length / pageSize);
|
||||
|
||||
const handlePageChange = (pageNum) => gotoPage(pageNum - 1);
|
||||
|
||||
|
|
|
@ -1,76 +1,3 @@
|
|||
/// https://github.com/mui-org/material-ui/blob/next/examples/nextjs/src/Link.js
|
||||
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||
import { Text } from '@mantine/core';
|
||||
import clsx from 'clsx';
|
||||
import NextLink from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export const NextLinkComposed = forwardRef(function NextLinkComposed(props: any, ref) {
|
||||
const { to, linkAs, href, replace, scroll, passHref, shallow, prefetch, locale, ...other } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<NextLink
|
||||
href={to}
|
||||
prefetch={prefetch}
|
||||
as={linkAs}
|
||||
replace={replace}
|
||||
scroll={scroll}
|
||||
shallow={shallow}
|
||||
passHref={passHref}
|
||||
locale={locale}
|
||||
>
|
||||
<a ref={ref} {...other} />
|
||||
</NextLink>
|
||||
);
|
||||
});
|
||||
|
||||
// A styled version of the Next.js Link component:
|
||||
// https://nextjs.org/docs/#with-link
|
||||
const Link = forwardRef(function Link(props: any, ref) {
|
||||
const {
|
||||
activeClassName = 'active',
|
||||
as: linkAs,
|
||||
className: classNameProps,
|
||||
href,
|
||||
noLinkStyle,
|
||||
role, // Link don't have roles.
|
||||
...other
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = typeof href === 'string' ? href : href.pathname;
|
||||
const className = clsx(classNameProps, {
|
||||
[activeClassName]: router.pathname === pathname && activeClassName,
|
||||
});
|
||||
|
||||
const isExternal =
|
||||
typeof href === 'string' && (href.indexOf('http') === 0 || href.indexOf('mailto:') === 0);
|
||||
|
||||
if (isExternal) {
|
||||
if (noLinkStyle) {
|
||||
return <Text variant='link' component='a' className={className} href={href} ref={ref} {...other} />;
|
||||
}
|
||||
|
||||
return <Text component='a' variant='link' href={href} ref={ref} {...other} />;
|
||||
}
|
||||
|
||||
if (noLinkStyle) {
|
||||
return <NextLinkComposed className={className} ref={ref} to={href} {...other} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
component={NextLinkComposed}
|
||||
variant='link'
|
||||
linkAs={linkAs}
|
||||
className={className}
|
||||
ref={ref}
|
||||
to={href}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
});
|
||||
import { NextLink as Link } from '@mantine/next';
|
||||
|
||||
export default Link;
|
46
src/components/Type.tsx
Normal file
46
src/components/Type.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { Group, Image, Stack, Text } from '@mantine/core';
|
||||
import { Prism } from '@mantine/prism';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AudioIcon, FileIcon, PlayIcon, TypeIcon, VideoIcon } from './icons';
|
||||
import MutedText from './MutedText';
|
||||
|
||||
function Placeholder({ text, Icon, ...props }) {
|
||||
return (
|
||||
<Image height={200} withPlaceholder placeholder={
|
||||
<Group>
|
||||
<Icon size={48} />
|
||||
<Text>{text}</Text>
|
||||
</Group>
|
||||
} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export default function Type({ file, popup = false, ...props }){
|
||||
const type = (file.type || file.mimetype).split('/')[0];
|
||||
const name = (file.name || file.file);
|
||||
|
||||
const [text, setText] = useState('');
|
||||
|
||||
if (type === 'text') {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch('/r/' + name);
|
||||
const text = await res.text();
|
||||
|
||||
setText(text);
|
||||
})();
|
||||
}, []);
|
||||
}
|
||||
|
||||
return popup ? {
|
||||
'video': <video width='100%' autoPlay controls {...props} />,
|
||||
'image': <Image {...props} />,
|
||||
'audio': <audio autoPlay controls {...props} style={{ width: '100%' }}/>,
|
||||
'text': <Prism withLineNumbers language={name.split('.').pop()} {...props} style={{}} sx={{}}>{text}</Prism>,
|
||||
}[type] : {
|
||||
'video': <Placeholder Icon={PlayIcon} text={`Click to view video (${name})`} {...props} />,
|
||||
'image': <Image {...props} />,
|
||||
'audio': <Placeholder Icon={AudioIcon} text={`Click to view audio (${name})`} {...props}/>,
|
||||
'text': <Placeholder Icon={FileIcon} text={`Click to view text file (${name})`} {...props}/>,
|
||||
}[type];
|
||||
};
|
|
@ -1,18 +1,12 @@
|
|||
/* eslint-disable jsx-a11y/alt-text */
|
||||
import React from 'react';
|
||||
import { Image, Table, Tooltip, Badge, useMantineTheme } from '@mantine/core';
|
||||
import { Table, Tooltip, Badge, useMantineTheme } from '@mantine/core';
|
||||
import Type from 'components/Type';
|
||||
|
||||
export function FilePreview({ file }: { file: File }) {
|
||||
const Type = props => {
|
||||
return {
|
||||
'video': <video autoPlay controls {...props} />,
|
||||
'image': <Image withPlaceholder {...props} />,
|
||||
'audio': <audio autoPlay controls {...props} />,
|
||||
}[file.type.split('/')[0]];
|
||||
};
|
||||
|
||||
return (
|
||||
<Type
|
||||
file={file}
|
||||
autoPlay
|
||||
sx={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
||||
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
||||
src={URL.createObjectURL(file)}
|
||||
|
|
5
src/components/icons/AudioIcon.tsx
Normal file
5
src/components/icons/AudioIcon.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Disc } from 'react-feather';
|
||||
|
||||
export default function AudioIcon({ ...props }) {
|
||||
return <Disc size={15} {...props} />;
|
||||
}
|
5
src/components/icons/CalendarIcon.tsx
Normal file
5
src/components/icons/CalendarIcon.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Calendar } from 'react-feather';
|
||||
|
||||
export default function CalendarIcon({ ...props }) {
|
||||
return <Calendar size={15} {...props} />;
|
||||
}
|
5
src/components/icons/HashIcon.tsx
Normal file
5
src/components/icons/HashIcon.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Hash } from 'react-feather';
|
||||
|
||||
export default function HashIcon({ ...props }) {
|
||||
return <Hash size={15} {...props} />;
|
||||
}
|
5
src/components/icons/PlayIcon.tsx
Normal file
5
src/components/icons/PlayIcon.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Play } from 'react-feather';
|
||||
|
||||
export default function PlayIcon({ ...props }) {
|
||||
return <Play size={15} {...props} />;
|
||||
}
|
5
src/components/icons/VideoIcon.tsx
Normal file
5
src/components/icons/VideoIcon.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Video } from 'react-feather';
|
||||
|
||||
export default function VideoIcon({ ...props }) {
|
||||
return <Video size={15} {...props} />;
|
||||
}
|
|
@ -16,6 +16,11 @@ import EnterIcon from './EnterIcon';
|
|||
import PlusIcon from './PlusIcon';
|
||||
import ImageIcon from './ImageIcon';
|
||||
import StarIcon from './StarIcon';
|
||||
import AudioIcon from './AudioIcon';
|
||||
import VideoIcon from './VideoIcon';
|
||||
import PlayIcon from './PlayIcon';
|
||||
import CalendarIcon from './CalendarIcon';
|
||||
import HashIcon from './HashIcon';
|
||||
|
||||
export {
|
||||
ActivityIcon,
|
||||
|
@ -36,4 +41,9 @@ export {
|
|||
PlusIcon,
|
||||
ImageIcon,
|
||||
StarIcon,
|
||||
AudioIcon,
|
||||
VideoIcon,
|
||||
PlayIcon,
|
||||
CalendarIcon,
|
||||
HashIcon,
|
||||
};
|
|
@ -42,6 +42,8 @@ export default function Upload() {
|
|||
|
||||
req.open('POST', '/api/upload');
|
||||
req.setRequestHeader('Authorization', user.token);
|
||||
req.setRequestHeader('UploadText', 'true');
|
||||
|
||||
req.send(body);
|
||||
};
|
||||
|
||||
|
@ -51,7 +53,7 @@ export default function Upload() {
|
|||
|
||||
<CodeInput
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value.trim())}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
|
||||
<Group position='right' mt='md'>
|
||||
|
|
|
@ -79,7 +79,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
const image = await prisma.image.create({
|
||||
data: {
|
||||
file: `${fileName}.${ext}`,
|
||||
mimetype: file.mimetype,
|
||||
mimetype: req.headers.uploadtext ? 'text/plain' : file.mimetype,
|
||||
userId: user.id,
|
||||
embed: !!req.headers.embed,
|
||||
format,
|
||||
|
|
|
@ -79,7 +79,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
|
||||
// @ts-ignore
|
||||
images.map(image => image.url = `/r/${image.file}`);
|
||||
if (req.query.filter && req.query.filter === 'media') images = images.filter(x => /^(video|audio|image)/.test(x.mimetype));
|
||||
if (req.query.filter && req.query.filter === 'media') images = images.filter(x => /^(video|audio|image|text)/.test(x.mimetype));
|
||||
|
||||
return res.json(req.query.paged ? chunk(images, 16) : images);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue