feat: public folders
This commit is contained in:
parent
6955d83b0c
commit
fc02dc02e8
11 changed files with 255 additions and 71 deletions
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Folder" ADD COLUMN "public" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -30,6 +30,7 @@ model User {
|
|||
model Folder {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
public Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
Group,
|
||||
LoadingOverlay,
|
||||
Modal,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
|
@ -15,10 +14,9 @@ import {
|
|||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { invalidateFiles, useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||
import { invalidateFolders, useFolders } from 'lib/queries/folders';
|
||||
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||
import { useFolders } from 'lib/queries/folders';
|
||||
import { relativeTime } from 'lib/utils/client';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
CalendarIcon,
|
||||
|
@ -30,12 +28,12 @@ import {
|
|||
ExternalLinkIcon,
|
||||
EyeIcon,
|
||||
FileIcon,
|
||||
HashIcon,
|
||||
ImageIcon,
|
||||
StarIcon,
|
||||
InfoIcon,
|
||||
FolderMinusIcon,
|
||||
FolderPlusIcon,
|
||||
HashIcon,
|
||||
ImageIcon,
|
||||
InfoIcon,
|
||||
StarIcon,
|
||||
} from './icons';
|
||||
import MutedText from './MutedText';
|
||||
import Type from './Type';
|
||||
|
@ -62,13 +60,18 @@ export function FileMeta({ Icon, title, subtitle, ...other }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default function File({ image, disableMediaPreview, exifEnabled, refreshImages }) {
|
||||
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 router = useRouter();
|
||||
|
||||
const folders = useFolders();
|
||||
|
||||
|
@ -257,7 +260,7 @@ export default function File({ image, disableMediaPreview, exifEnabled, refreshI
|
|||
subtitle={relativeTime(new Date(image.createdAt))}
|
||||
tooltip={new Date(image?.createdAt).toLocaleString()}
|
||||
/>
|
||||
{image.expiresAt && (
|
||||
{image.expiresAt && !reducedActions && (
|
||||
<FileMeta
|
||||
Icon={ClockIcon}
|
||||
title='Expires'
|
||||
|
@ -271,7 +274,7 @@ export default function File({ image, disableMediaPreview, exifEnabled, refreshI
|
|||
|
||||
<Group position='apart' my='md'>
|
||||
<Group position='left'>
|
||||
{exifEnabled && (
|
||||
{exifEnabled && !reducedActions && (
|
||||
<Tooltip label='View Metadata'>
|
||||
<ActionIcon
|
||||
color='blue'
|
||||
|
@ -282,7 +285,7 @@ export default function File({ image, disableMediaPreview, exifEnabled, refreshI
|
|||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{inFolder && !folders.isLoading ? (
|
||||
{reducedActions ? null : inFolder && !folders.isLoading ? (
|
||||
<Tooltip
|
||||
label={`Remove from folder "${
|
||||
folders.data.find((f) => f.id === image.folderId)?.name ?? ''
|
||||
|
@ -317,6 +320,8 @@ export default function File({ image, disableMediaPreview, exifEnabled, refreshI
|
|||
)}
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
{reducedActions ? null : (
|
||||
<>
|
||||
<Tooltip label='Delete file'>
|
||||
<ActionIcon color='red' variant='filled' onClick={handleDelete}>
|
||||
<DeleteIcon />
|
||||
|
@ -332,6 +337,8 @@ export default function File({ image, disableMediaPreview, exifEnabled, refreshI
|
|||
<StarIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tooltip label='Open in new tab'>
|
||||
<ActionIcon color='blue' variant='filled' onClick={() => window.open(image.url, '_blank')}>
|
||||
|
|
5
src/components/icons/LockIcon.tsx
Normal file
5
src/components/icons/LockIcon.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Lock } from 'react-feather';
|
||||
|
||||
export default function LockIcon({ ...props }) {
|
||||
return <Lock size={15} {...props} />;
|
||||
}
|
5
src/components/icons/UnlockIcon.tsx
Normal file
5
src/components/icons/UnlockIcon.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Unlock } from 'react-feather';
|
||||
|
||||
export default function UnlockIcon({ ...props }) {
|
||||
return <Unlock size={15} {...props} />;
|
||||
}
|
|
@ -39,6 +39,8 @@ import FolderIcon from './FolderIcon';
|
|||
import FolderMinusIcon from './FolderMinusIcon';
|
||||
import FolderPlusIcon from './FolderPlusIcon';
|
||||
import GlobeIcon from './GlobeIcon';
|
||||
import LockIcon from './LockIcon';
|
||||
import UnlockIcon from './UnlockIcon';
|
||||
|
||||
export {
|
||||
ActivityIcon,
|
||||
|
@ -82,4 +84,6 @@ export {
|
|||
FolderMinusIcon,
|
||||
FolderPlusIcon,
|
||||
GlobeIcon,
|
||||
LockIcon,
|
||||
UnlockIcon,
|
||||
};
|
||||
|
|
|
@ -2,7 +2,8 @@ import { ActionIcon, Avatar, Card, Group, SimpleGrid, Skeleton, Stack, Title, To
|
|||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { CopyIcon, DeleteIcon, FileIcon, PlusIcon } from 'components/icons';
|
||||
import { DeleteIcon, FileIcon, PlusIcon, LockIcon, UnlockIcon, LinkIcon, CopyIcon } from 'components/icons';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useFolders } from 'lib/queries/folders';
|
||||
|
@ -65,6 +66,30 @@ export default function Folders({ disableMediaPreview, exifEnabled }) {
|
|||
});
|
||||
};
|
||||
|
||||
const makePublic = async (folder) => {
|
||||
const res = await useFetch(`/api/user/folders/${folder.id}`, 'PATCH', {
|
||||
public: folder.public ? false : true,
|
||||
});
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Made folder public',
|
||||
message: `Made folder ${folder.name} ${folder.public ? 'private' : 'public'}`,
|
||||
color: 'green',
|
||||
icon: <UnlockIcon />,
|
||||
});
|
||||
folders.refetch();
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to make folder public/private',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <UnlockIcon />,
|
||||
});
|
||||
folders.refetch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateFolderModal
|
||||
|
@ -101,6 +126,7 @@ export default function Folders({ disableMediaPreview, exifEnabled }) {
|
|||
<Stack spacing={0}>
|
||||
<Title>{folder.name}</Title>
|
||||
<MutedText size='sm'>ID: {folder.id}</MutedText>
|
||||
<MutedText size='sm'>Public: {folder.public ? 'Yes' : 'No'}</MutedText>
|
||||
<Tooltip label={new Date(folder.createdAt).toLocaleString()}>
|
||||
<div>
|
||||
<MutedText size='sm'>
|
||||
|
@ -117,7 +143,15 @@ export default function Folders({ disableMediaPreview, exifEnabled }) {
|
|||
</Tooltip>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Stack>
|
||||
<Group>
|
||||
<Tooltip label={folder.public ? 'Make folder private' : 'Make folder public'}>
|
||||
<ActionIcon
|
||||
aria-label={folder.public ? 'make private' : 'make public'}
|
||||
onClick={() => makePublic(folder)}
|
||||
>
|
||||
{folder.public ? <LockIcon /> : <UnlockIcon />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<ActionIcon
|
||||
aria-label='view files'
|
||||
onClick={() => {
|
||||
|
@ -127,10 +161,28 @@ export default function Folders({ disableMediaPreview, exifEnabled }) {
|
|||
>
|
||||
<FileIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
aria-label='copy link'
|
||||
onClick={() => {
|
||||
clipboard.copy(`${window.location.origin}/folder/${folder.id}`);
|
||||
showNotification({
|
||||
title: 'Copied folder link',
|
||||
message: (
|
||||
<>
|
||||
Copied <Link href={`/folder/${folder.id}`}>folder link</Link> to clipboard
|
||||
</>
|
||||
),
|
||||
color: 'green',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<LinkIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon aria-label='delete' onClick={() => deleteFolder(folder)}>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
|
|
|
@ -8,6 +8,7 @@ export type UserFoldersResponse = {
|
|||
userId: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
public: boolean;
|
||||
files?: UserFilesResponse[];
|
||||
};
|
||||
|
||||
|
@ -40,46 +41,6 @@ export const useFolder = (id: string, withFiles: boolean = false) => {
|
|||
});
|
||||
};
|
||||
|
||||
// export function useFileDelete() {
|
||||
// // '/api/user/files', 'DELETE', { id: image.id }
|
||||
// return useMutation(
|
||||
// async (id: string) => {
|
||||
// return fetch('/api/user/files', {
|
||||
// method: 'DELETE',
|
||||
// body: JSON.stringify({ id }),
|
||||
// headers: {
|
||||
// 'content-type': 'application/json',
|
||||
// },
|
||||
// }).then((res) => res.json());
|
||||
// },
|
||||
// {
|
||||
// onSuccess: () => {
|
||||
// queryClient.refetchQueries(['files']);
|
||||
// },
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
// export function useFileFavorite() {
|
||||
// // /api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite }
|
||||
// return useMutation(
|
||||
// async (data: { id: string; favorite: boolean }) => {
|
||||
// return fetch('/api/user/files', {
|
||||
// method: 'PATCH',
|
||||
// body: JSON.stringify(data),
|
||||
// headers: {
|
||||
// 'content-type': 'application/json',
|
||||
// },
|
||||
// }).then((res) => res.json());
|
||||
// },
|
||||
// {
|
||||
// onSuccess: () => {
|
||||
// queryClient.refetchQueries(['files']);
|
||||
// },
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
export function invalidateFolders() {
|
||||
return queryClient.invalidateQueries(['folders']);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
public: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -75,6 +76,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
public: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -96,6 +98,40 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
}
|
||||
}
|
||||
|
||||
return res.json(folder);
|
||||
} else if (req.method === 'PATCH') {
|
||||
const { public: publicFolder } = req.body as { public?: string };
|
||||
|
||||
const folder = await prisma.folder.update({
|
||||
where: {
|
||||
id: idParsed,
|
||||
},
|
||||
data: {
|
||||
public: !!publicFolder,
|
||||
},
|
||||
select: {
|
||||
files: !!req.query.files,
|
||||
id: true,
|
||||
name: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
public: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (req.query.files) {
|
||||
for (let i = 0; i !== folder.files.length; ++i) {
|
||||
const file = folder.files[i];
|
||||
delete file.password;
|
||||
|
||||
(folder.files[i] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
folder.files[i].name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return res.json(folder);
|
||||
} else if (req.method === 'DELETE') {
|
||||
const deletingFolder = !!req.body.deleteFolder;
|
||||
|
@ -111,6 +147,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
public: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -167,6 +204,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
public: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -208,6 +246,6 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
}
|
||||
|
||||
export default withZipline(handler, {
|
||||
methods: ['GET', 'POST', 'DELETE'],
|
||||
methods: ['GET', 'POST', 'DELETE', 'PATCH'],
|
||||
user: true,
|
||||
});
|
||||
|
|
|
@ -69,6 +69,10 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
|
|||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
public: true,
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
105
src/pages/folder/[id].tsx
Normal file
105
src/pages/folder/[id].tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
import { Container, SimpleGrid, Title } from '@mantine/core';
|
||||
import File from 'components/File';
|
||||
import prisma from 'lib/prisma';
|
||||
import { formatRootUrl } from 'lib/utils/urls';
|
||||
import { GetServerSideProps } from 'next';
|
||||
|
||||
type LimitedFolder = {
|
||||
files: {
|
||||
id: number;
|
||||
name: string;
|
||||
createdAt: Date | string;
|
||||
mimetype: string;
|
||||
views: number;
|
||||
}[];
|
||||
user: {
|
||||
username: string;
|
||||
};
|
||||
name: string;
|
||||
public: boolean;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
folder: LimitedFolder;
|
||||
uploadRoute: string;
|
||||
};
|
||||
|
||||
export default function EmbeddedFile({ folder }: Props) {
|
||||
return (
|
||||
<Container size='lg'>
|
||||
<Title align='center' my='lg'>
|
||||
Viewing folder: {folder.name}
|
||||
</Title>
|
||||
<SimpleGrid
|
||||
my='md'
|
||||
cols={3}
|
||||
breakpoints={[
|
||||
{ maxWidth: 600, cols: 1 },
|
||||
{ maxWidth: 900, cols: 2 },
|
||||
{ maxWidth: 1200, cols: 3 },
|
||||
]}
|
||||
>
|
||||
{folder.files.map((file, i) => (
|
||||
<File
|
||||
key={i}
|
||||
image={file}
|
||||
disableMediaPreview={false}
|
||||
exifEnabled={false}
|
||||
refreshImages={null}
|
||||
reducedActions={true}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
|
||||
const { id } = context.params as { id: string };
|
||||
|
||||
if (isNaN(Number(id))) return { notFound: true };
|
||||
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
id: Number(id),
|
||||
},
|
||||
select: {
|
||||
files: {
|
||||
select: {
|
||||
name: true,
|
||||
mimetype: true,
|
||||
id: true,
|
||||
views: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
name: true,
|
||||
public: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!folder) return { notFound: true };
|
||||
if (!folder.public) return { notFound: true };
|
||||
|
||||
for (let j = 0; j !== folder.files.length; ++j) {
|
||||
(folder.files[j] as unknown as { url: string }).url = formatRootUrl(
|
||||
config.uploader.route,
|
||||
folder.files[j].name
|
||||
);
|
||||
|
||||
(folder.files[j].createdAt as unknown) = folder.files[j].createdAt.toString();
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
folder,
|
||||
uploadRoute: config.uploader.route,
|
||||
},
|
||||
};
|
||||
};
|
Loading…
Reference in a new issue