feat: revamp file gallery

This commit is contained in:
dicedtomato 2022-07-12 22:09:57 +00:00 committed by GitHub
parent b7560c80aa
commit 56ff86db44
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 171 additions and 122 deletions

View file

@ -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"
}
}

View file

@ -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)}

View file

@ -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);

View file

@ -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
View 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];
};

View file

@ -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)}

View file

@ -0,0 +1,5 @@
import { Disc } from 'react-feather';
export default function AudioIcon({ ...props }) {
return <Disc size={15} {...props} />;
}

View file

@ -0,0 +1,5 @@
import { Calendar } from 'react-feather';
export default function CalendarIcon({ ...props }) {
return <Calendar size={15} {...props} />;
}

View file

@ -0,0 +1,5 @@
import { Hash } from 'react-feather';
export default function HashIcon({ ...props }) {
return <Hash size={15} {...props} />;
}

View file

@ -0,0 +1,5 @@
import { Play } from 'react-feather';
export default function PlayIcon({ ...props }) {
return <Play size={15} {...props} />;
}

View file

@ -0,0 +1,5 @@
import { Video } from 'react-feather';
export default function VideoIcon({ ...props }) {
return <Video size={15} {...props} />;
}

View file

@ -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,
};

View file

@ -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'>

View file

@ -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,

View file

@ -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);
}