From fc02dc02e8d1b37e96585b8c08b7733dcbda8cc6 Mon Sep 17 00:00:00 2001 From: diced Date: Thu, 23 Feb 2023 14:37:22 -0800 Subject: [PATCH] feat: public folders --- .../migration.sql | 2 + prisma/schema.prisma | 1 + src/components/File.tsx | 61 +++++----- src/components/icons/LockIcon.tsx | 5 + src/components/icons/UnlockIcon.tsx | 5 + src/components/icons/index.tsx | 4 + src/components/pages/Folders/index.tsx | 58 +++++++++- src/lib/queries/folders.ts | 41 +------ src/pages/api/user/folders/[id].ts | 40 ++++++- src/pages/api/user/folders/index.ts | 4 + src/pages/folder/[id].tsx | 105 ++++++++++++++++++ 11 files changed, 255 insertions(+), 71 deletions(-) create mode 100644 prisma/migrations/20230219001919_public_folder/migration.sql create mode 100644 src/components/icons/LockIcon.tsx create mode 100644 src/components/icons/UnlockIcon.tsx create mode 100644 src/pages/folder/[id].tsx diff --git a/prisma/migrations/20230219001919_public_folder/migration.sql b/prisma/migrations/20230219001919_public_folder/migration.sql new file mode 100644 index 0000000..1793317 --- /dev/null +++ b/prisma/migrations/20230219001919_public_folder/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Folder" ADD COLUMN "public" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 06f66df..1017b3e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 diff --git a/src/components/File.tsx b/src/components/File.tsx index 6298bc3..85776fc 100644 --- a/src/components/File.tsx +++ b/src/components/File.tsx @@ -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 && ( - {exifEnabled && ( + {exifEnabled && !reducedActions && ( )} - {inFolder && !folders.isLoading ? ( + {reducedActions ? null : inFolder && !folders.isLoading ? ( f.id === image.folderId)?.name ?? '' @@ -317,21 +320,25 @@ export default function File({ image, disableMediaPreview, exifEnabled, refreshI )} - - - - - + {reducedActions ? null : ( + <> + + + + + - - - - - + + + + + + + )} window.open(image.url, '_blank')}> diff --git a/src/components/icons/LockIcon.tsx b/src/components/icons/LockIcon.tsx new file mode 100644 index 0000000..defa24f --- /dev/null +++ b/src/components/icons/LockIcon.tsx @@ -0,0 +1,5 @@ +import { Lock } from 'react-feather'; + +export default function LockIcon({ ...props }) { + return ; +} diff --git a/src/components/icons/UnlockIcon.tsx b/src/components/icons/UnlockIcon.tsx new file mode 100644 index 0000000..0008ce5 --- /dev/null +++ b/src/components/icons/UnlockIcon.tsx @@ -0,0 +1,5 @@ +import { Unlock } from 'react-feather'; + +export default function UnlockIcon({ ...props }) { + return ; +} diff --git a/src/components/icons/index.tsx b/src/components/icons/index.tsx index 4287ed3..f9d4ae9 100644 --- a/src/components/icons/index.tsx +++ b/src/components/icons/index.tsx @@ -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, }; diff --git a/src/components/pages/Folders/index.tsx b/src/components/pages/Folders/index.tsx index 4bb9de1..54b4c2a 100644 --- a/src/components/pages/Folders/index.tsx +++ b/src/components/pages/Folders/index.tsx @@ -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: , + }); + folders.refetch(); + } else { + showNotification({ + title: 'Failed to make folder public/private', + message: res.error, + color: 'red', + icon: , + }); + folders.refetch(); + } + }; + return ( <> {folder.name} ID: {folder.id} + Public: {folder.public ? 'Yes' : 'No'}
@@ -117,7 +143,15 @@ export default function Folders({ disableMediaPreview, exifEnabled }) { - + + + makePublic(folder)} + > + {folder.public ? : } + + { @@ -127,10 +161,28 @@ export default function Folders({ disableMediaPreview, exifEnabled }) { > + { + clipboard.copy(`${window.location.origin}/folder/${folder.id}`); + showNotification({ + title: 'Copied folder link', + message: ( + <> + Copied folder link to clipboard + + ), + color: 'green', + icon: , + }); + }} + > + + deleteFolder(folder)}> - + )) diff --git a/src/lib/queries/folders.ts b/src/lib/queries/folders.ts index ab8f756..986bbb3 100644 --- a/src/lib/queries/folders.ts +++ b/src/lib/queries/folders.ts @@ -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']); } diff --git a/src/pages/api/user/folders/[id].ts b/src/pages/api/user/folders/[id].ts index 9376620..f071515 100644 --- a/src/pages/api/user/folders/[id].ts +++ b/src/pages/api/user/folders/[id].ts @@ -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, }); diff --git a/src/pages/api/user/folders/index.ts b/src/pages/api/user/folders/index.ts index 902a41f..1d4993a 100644 --- a/src/pages/api/user/folders/index.ts +++ b/src/pages/api/user/folders/index.ts @@ -69,6 +69,10 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { userId: true, createdAt: true, updatedAt: true, + public: true, + }, + orderBy: { + updatedAt: 'desc', }, }); diff --git a/src/pages/folder/[id].tsx b/src/pages/folder/[id].tsx new file mode 100644 index 0000000..72b040c --- /dev/null +++ b/src/pages/folder/[id].tsx @@ -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 ( + + + Viewing folder: {folder.name} + + + {folder.files.map((file, i) => ( + + ))} + + + ); +} + +export const getServerSideProps: GetServerSideProps = 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, + }, + }; +};