diff --git a/src/components/File.tsx b/src/components/File.tsx deleted file mode 100644 index fb46fe6..0000000 --- a/src/components/File.tsx +++ /dev/null @@ -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 ? ( - - - - - {title} - {subtitle} - - - - ) : ( - - - - {title} - {subtitle} - - - ); -} - -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: , - }); - }, - - onError: (res: any) => { - showNotification({ - title: 'Failed to delete file', - message: res.error, - color: 'red', - icon: , - }); - }, - - 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: , - }); - }; - - const handleFavorite = async () => { - favoriteFile.mutate( - { id: image.id, favorite: !image.favorite }, - { - onSuccess: () => { - showNotification({ - title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'), - message: '', - icon: , - }); - }, - - onError: (res: any) => { - showNotification({ - title: 'Failed to favorite file', - message: res.error, - color: 'red', - icon: , - }); - }, - } - ); - }; - - 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: , - }); - } else { - showNotification({ - title: 'Failed to remove from folder', - message: res.error, - color: 'red', - icon: , - }); - } - }; - - 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: , - }); - } else { - showNotification({ - title: 'Failed to add to folder', - message: res.error, - color: 'red', - icon: , - }); - } - }; - - 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: , - }); - } else { - showNotification({ - title: 'Failed to create folder', - message: res.error, - color: 'red', - icon: , - }); - } - }); - return { value: t, label: t }; - }; - - return ( - <> - setOpen(false)} title={{image.name}} size='xl'> - - - - - - - - {image.maxViews && ( - - )} - - {image.expiresAt && !reducedActions && ( - - )} - - - - - - - {exifEnabled && !reducedActions && ( - - window.open(`/dashboard/metadata/${image.id}`, '_blank')} - > - - - - )} - {reducedActions ? null : inFolder && !folders.isLoading ? ( - f.id === image.folderId)?.name ?? '' - }"`} - > - - - - - ) : ( - - ({ + value: String(folder.id), + label: `${folder.id}: ${folder.name}`, + })), + ]} + searchable + creatable + getCreateLabel={(query) => `Create folder "${query}"`} + onCreate={createFolder} + /> + + )} + + + {reducedActions ? null : ( + <> + + + + + + + + + + + + + )} + + + window.open(file.url, '_blank')}> + + + + + + + + + + + + window.open(`/r/${encodeURI(file.name)}?download=true`, '_blank')} + > + + + + + + + ); +} diff --git a/src/components/File/index.tsx b/src/components/File/index.tsx new file mode 100644 index 0000000..8bfef83 --- /dev/null +++ b/src/components/File/index.tsx @@ -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 ? ( + + + + + {title} + {subtitle} + + + + ) : ( + + + + {title} + {subtitle} + + + ); +} + +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 ( + <> + + + + + + setOpen(true)} + disableMediaPreview={disableMediaPreview} + /> + + + + ); +} diff --git a/src/components/Type.tsx b/src/components/Type.tsx index b66aeda..099d68f 100644 --- a/src/components/Type.tsx +++ b/src/components/Type.tsx @@ -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 ( 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()} {shouldRenderMarkdown && } {shouldRenderTex && } @@ -90,7 +78,7 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop ); if (media && disableMediaPreview) { - return ; + return ; } return popup ? ( @@ -110,7 +98,7 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop ) : ( <> - {(shouldRenderMarkdown || shouldRenderTex) && renderRenderAlert()} + {(shouldRenderMarkdown || shouldRenderTex) && renderAlert()} )} diff --git a/src/components/pages/Dashboard/index.tsx b/src/components/pages/Dashboard/index.tsx index 166eac5..fce10da 100644 --- a/src/components/pages/Dashboard/index.tsx +++ b/src/components/pages/Dashboard/index.tsx @@ -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({ + 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: , }); @@ -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: ( {`${window.location.protocol}//${window.location.host}${original.url}`} + href={`${window.location.protocol}//${window.location.host}${file.url}`} + >{`${window.location.protocol}//${window.location.host}${file.url}`} ), icon: , }); }; - 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 (
+ {selectedFile && ( + files.refetch()} + reducedActions={false} + exifEnabled={exifEnabled} + /> + )} + Welcome back, {user?.username} - You have {images.isSuccess ? images.data.length : '...'} files + You have {numFiles === 0 ? '...' : numFiles} files @@ -90,74 +148,92 @@ export default function Dashboard({ disableMediaPreview, exifEnabled }) { View your gallery here. - } + + new Date(file.createdAt).toLocaleString(), }, { - accessorKey: 'mimetype', - header: 'Type', - filterFn: stringFilterFn, - }, - { - accessorKey: 'createdAt', - header: 'Date', - filterFn: dateFilterFn, + accessor: 'actions', + textAlignment: 'right', + render: (file) => ( + + + { + setSelectedFile(file); + setOpen(true); + }} + color='blue' + > + + + + + + viewFile(file)} color='blue'> + + + + + copyFile(file)} color='green'> + + + deleteFile(file)} color='red'> + + + + ), }, ]} + 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: , + title: `View ${file.name}`, + onClick: () => viewFile(file), + }, + { + key: 'copy', + icon: , + title: `Copy ${file.name}`, + onClick: () => copyFile(file), + }, + { + key: 'delete', + icon: , + title: `Delete ${file.name}`, + onClick: () => deleteFile(file), + }, + ], + }} + onCellClick={({ record: file }) => { + setSelectedFile(file); + setOpen(true); + }} />
diff --git a/src/pages/api/user/files.ts b/src/pages/api/user/files.ts index 9afc7f0..488d3cc 100644 --- a/src/pages/api/user/files.ts +++ b/src/pages/api/user/files.ts @@ -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; diff --git a/tsconfig.json b/tsconfig.json index 303cf22..726cd7c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] } diff --git a/yarn.lock b/yarn.lock index 9a34d2a..63342d6 100644 --- a/yarn.lock +++ b/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