From 6dc9de21f30ce2848927c2903da2e120a988678a Mon Sep 17 00:00:00 2001 From: kelvin Date: Sun, 8 Dec 2024 20:16:13 +0300 Subject: [PATCH 1/2] feat: added file manager support and update dependencies Introduced a new file manager route and associated types to handle file operations effectively. Added mock data for file activities and folders to simulate the file management functionality. Updated the Mantine and Tiptap dependencies to their latest versions for improved performance and new features. --- .../ActionButton/ActionButton.module.css | 27 + .../components/ActionButton/ActionButton.tsx | 31 + .../FileButton/FileButton.module.css | 14 + .../components/FileButton/FileButton.tsx | 41 + .../file-manager/components/FilesTable.tsx | 111 +++ app/apps/file-manager/components/index.ts | 3 + app/apps/file-manager/layout.tsx | 157 ++++ app/apps/file-manager/page.module.css | 0 app/apps/file-manager/page.tsx | 201 +++++ app/apps/file-manager/types/index.ts | 64 ++ app/apps/file-manager/utils/index.ts | 87 ++ app/error.module.css | 2 +- app/error.tsx | 16 +- app/layout.tsx | 2 + app/not-found.tsx | 10 +- components/Navigation/Navigation.tsx | 6 + package.json | 36 +- pnpm-lock.yaml | 816 +++++++++--------- public/mocks/FileActivities.json | 88 ++ public/mocks/Files.json | 211 +++++ public/mocks/Folders.json | 96 +++ routes/index.ts | 3 + theme/index.ts | 4 +- 23 files changed, 1611 insertions(+), 415 deletions(-) create mode 100644 app/apps/file-manager/components/ActionButton/ActionButton.module.css create mode 100644 app/apps/file-manager/components/ActionButton/ActionButton.tsx create mode 100644 app/apps/file-manager/components/FileButton/FileButton.module.css create mode 100644 app/apps/file-manager/components/FileButton/FileButton.tsx create mode 100644 app/apps/file-manager/components/FilesTable.tsx create mode 100644 app/apps/file-manager/components/index.ts create mode 100644 app/apps/file-manager/layout.tsx create mode 100644 app/apps/file-manager/page.module.css create mode 100644 app/apps/file-manager/page.tsx create mode 100644 app/apps/file-manager/types/index.ts create mode 100644 app/apps/file-manager/utils/index.ts create mode 100644 public/mocks/FileActivities.json create mode 100644 public/mocks/Files.json create mode 100644 public/mocks/Folders.json diff --git a/app/apps/file-manager/components/ActionButton/ActionButton.module.css b/app/apps/file-manager/components/ActionButton/ActionButton.module.css new file mode 100644 index 0000000..ff21713 --- /dev/null +++ b/app/apps/file-manager/components/ActionButton/ActionButton.module.css @@ -0,0 +1,27 @@ +.wrapper { + padding: var(--mantine-spacing-md); + background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); + border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + border-radius: var(--mantine-radius-default); + box-shadow: var(--mantine-shadow-md); + + &[data-primary="true"] { + background-color: var(--mantine-primary-color-filled); + border-color: var(--mantine-primary-color-filled); + color: var(--mantine-color-white); + } + + @mixin hover { + transition: all ease 150ms; + border-color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-2)); + background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); + + &[data-primary="true"] { + background-color: var(--mantine-primary-color-filled-hover); + } + } +} + +.label { + font-weight: 500; +} diff --git a/app/apps/file-manager/components/ActionButton/ActionButton.tsx b/app/apps/file-manager/components/ActionButton/ActionButton.tsx new file mode 100644 index 0000000..0ec4054 --- /dev/null +++ b/app/apps/file-manager/components/ActionButton/ActionButton.tsx @@ -0,0 +1,31 @@ +import { FC } from 'react'; + +import { Text, UnstyledButton, UnstyledButtonProps } from '@mantine/core'; + +import classes from './ActionButton.module.css'; + +type ActionButtonProps = UnstyledButtonProps & { + icon: FC; + label: string; + asPrimary?: boolean; +}; + +export function ActionButton({ + icon: Icon, + label, + asPrimary = false, + ...others +}: ActionButtonProps) { + return ( + + + + {label} + + + ); +} diff --git a/app/apps/file-manager/components/FileButton/FileButton.module.css b/app/apps/file-manager/components/FileButton/FileButton.module.css new file mode 100644 index 0000000..24386f4 --- /dev/null +++ b/app/apps/file-manager/components/FileButton/FileButton.module.css @@ -0,0 +1,14 @@ +.wrapper { + display: flex; + align-items: center; + gap: var(--mantine-spacing-sm); + border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + border-radius: var(--mantine-radius-default); + padding: var(--mantine-spacing-sm); + + @mixin hover { + transition: all ease 150ms; + border-color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); + background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); + } +} diff --git a/app/apps/file-manager/components/FileButton/FileButton.tsx b/app/apps/file-manager/components/FileButton/FileButton.tsx new file mode 100644 index 0000000..4e953d0 --- /dev/null +++ b/app/apps/file-manager/components/FileButton/FileButton.tsx @@ -0,0 +1,41 @@ +import { + Flex, + Stack, + Text, + UnstyledButton, + UnstyledButtonProps, +} from '@mantine/core'; +import { IconPointFilled } from '@tabler/icons-react'; + +import { IFile } from '@/app/apps/file-manager/types'; + +import classes from './FileButton.module.css'; +import { resolveFileIcon } from '../../utils'; + +type FileButtonProps = UnstyledButtonProps & { + file: IFile; +}; + +export function FileButton({ file, ...others }: FileButtonProps) { + const Icon = resolveFileIcon(file.type); + + return ( + + + + + {file.name} + + + + {file.size} + + + + {file.type} + + + + + ); +} diff --git a/app/apps/file-manager/components/FilesTable.tsx b/app/apps/file-manager/components/FilesTable.tsx new file mode 100644 index 0000000..1d11962 --- /dev/null +++ b/app/apps/file-manager/components/FilesTable.tsx @@ -0,0 +1,111 @@ +'use client'; + +import React, { ReactNode, useEffect, useState } from 'react'; + +import { Flex, Text, ThemeIcon } from '@mantine/core'; +import { useDebouncedValue } from '@mantine/hooks'; +import sortBy from 'lodash/sortBy'; +import { + DataTable, + DataTableProps, + DataTableSortStatus, +} from 'mantine-datatable'; + +import { IFile } from '@/app/apps/file-manager/types'; +import { resolveFileIcon } from '@/app/apps/file-manager/utils'; +import { ErrorAlert } from '@/components'; + +const PAGE_SIZES = [10, 15, 20]; + +type FilesTableProps = { + data: IFile[]; + error?: ReactNode; + loading?: boolean; +}; + +export function FilesTable({ data, loading, error }: FilesTableProps) { + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(PAGE_SIZES[2]); + const [selectedRecords, setSelectedRecords] = useState([]); + const [records, setRecords] = useState(data.slice(0, pageSize)); + const [sortStatus, setSortStatus] = useState({ + columnAccessor: 'product', + direction: 'asc', + }); + const [query, setQuery] = useState(''); + const [debouncedQuery] = useDebouncedValue(query, 200); + const [selectedStatuses, setSelectedStatuses] = useState([]); + + const columns: DataTableProps['columns'] = [ + { + accessor: 'name', + render: (item: IFile) => { + const Icon = resolveFileIcon(item.type); + + return ( + + + + + {item.name} + + ); + }, + }, + { + accessor: 'type', + }, + { + accessor: 'size', + }, + { + accessor: 'modified_at', + }, + { + accessor: 'owner', + }, + ]; + + useEffect(() => { + setPage(1); + }, [pageSize]); + + useEffect(() => { + const from = (page - 1) * pageSize; + const to = from + pageSize; + const d = sortBy(data, sortStatus.columnAccessor) as IFile[]; + const dd = d.slice(from, to) as IFile[]; + let filtered = sortStatus.direction === 'desc' ? dd.reverse() : dd; + + setRecords(filtered); + }, [sortStatus, data, page, pageSize, debouncedQuery, selectedStatuses]); + + return error ? ( + + ) : ( + 0 + ? records.length + : data.length + } + recordsPerPage={pageSize} + page={page} + onPageChange={(p) => setPage(p)} + recordsPerPageOptions={PAGE_SIZES} + onRecordsPerPageChange={setPageSize} + sortStatus={sortStatus} + onSortStatusChange={setSortStatus} + fetching={loading} + /> + ); +} diff --git a/app/apps/file-manager/components/index.ts b/app/apps/file-manager/components/index.ts new file mode 100644 index 0000000..244166a --- /dev/null +++ b/app/apps/file-manager/components/index.ts @@ -0,0 +1,3 @@ +export * from './ActionButton/ActionButton'; +export * from './FileButton/FileButton'; +export * from './FilesTable'; diff --git a/app/apps/file-manager/layout.tsx b/app/apps/file-manager/layout.tsx new file mode 100644 index 0000000..3594d7e --- /dev/null +++ b/app/apps/file-manager/layout.tsx @@ -0,0 +1,157 @@ +'use client'; + +import { ReactNode } from 'react'; + +import { DonutChart } from '@mantine/charts'; +import { + Button, + Container, + Flex, + Grid, + Paper, + PaperProps, + Stack, + Text, + ThemeIcon, + Timeline, + Title, + UnstyledButton, +} from '@mantine/core'; +import { IconChevronRight, IconPointFilled } from '@tabler/icons-react'; +import Link from 'next/link'; + +import { IFileActivity, IFolder } from '@/app/apps/file-manager/types'; +import { + resolveActionIcon, + resolveFileIcon, + resolveFolderIcon, +} from '@/app/apps/file-manager/utils'; +import { useFetchData } from '@/hooks'; + +const PAPER_PROPS: PaperProps = { + shadow: 'md', + radius: 'md', + p: 'md', + mb: 'md', +}; + +export default function FileManagerLayout({ + children, +}: { + children: ReactNode; +}) { + const { + data: foldersData, + loading: foldersLoading, + error: foldersError, + } = useFetchData('/mocks/Folders.json'); + const { + data: fileActivityData, + loading: fileActivityLoading, + error: fileActivityError, + } = useFetchData('/mocks/FileActivities.json'); + + const folders = foldersData.slice(0, 5).map((folder: IFolder) => { + const Icon = resolveFolderIcon(folder.name); + + return ( + + + + + + + {folder.name} + + + {folder.total_files} files + + {folder.estimated_size} + + + + ); + }); + + const fileActivityItems = fileActivityData.map( + (fileActivity: IFileActivity) => { + const ActionIcon = resolveActionIcon(fileActivity.action); + const FileIcon = resolveFileIcon(fileActivity.file_type); + + return ( + } + lineVariant="dashed" + title={ + + {fileActivity.user} + + {fileActivity.action} {fileActivity.file_type} + + + } + > + + + + {fileActivity.file_name} + + {fileActivity.timestamp} + + + ); + }, + ); + + return ( + <> + <> + File Manager | DesignSparx + + + + + {children} + + + + + Storage usage + + + {folders} + + + + Activity + + + + {fileActivityItems} + + + + + + + ); +} diff --git a/app/apps/file-manager/page.module.css b/app/apps/file-manager/page.module.css new file mode 100644 index 0000000..e69de29 diff --git a/app/apps/file-manager/page.tsx b/app/apps/file-manager/page.tsx new file mode 100644 index 0000000..1a77aa5 --- /dev/null +++ b/app/apps/file-manager/page.tsx @@ -0,0 +1,201 @@ +'use client'; + +import React, { useMemo, useState } from 'react'; + +import { + Anchor, + Button, + Flex, + Input, + Paper, + PaperProps, + SegmentedControl, + SimpleGrid, + Stack, + Text, + Title, + rem, +} from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; +import { + IconChevronRight, + IconCloudUpload, + IconEdit, + IconFolderPlus, + IconPhotoVideo, + IconPlus, + IconSearch, + IconUpload, + IconX, +} from '@tabler/icons-react'; + +import { + ActionButton, + FileButton, + FilesTable, +} from '@/app/apps/file-manager/components'; +import { PageHeader } from '@/components'; +import { useFetchData } from '@/hooks'; +import { PATH_DASHBOARD } from '@/routes'; + +import { IFile, IFolder } from './types'; + +const items = [ + { title: 'Dashboard', href: PATH_DASHBOARD.default }, + { title: 'Apps', href: '#' }, + { title: 'File manager', href: '#' }, +].map((item, index) => ( + + {item.title} + +)); + +const ICON_SIZE = 16; + +const PAPER_PROPS: PaperProps = { + shadow: 'md', + radius: 'md', + p: 'md', +}; + +function FileManager() { + const { + data: filesData, + loading: filesLoading, + error: filesError, + } = useFetchData('/mocks/Files.json'); + const { + data: foldersData, + loading: foldersLoading, + error: foldersError, + } = useFetchData('/mocks/Folders.json'); + const [value, setValue] = useState('View all'); + + const refinedFolders: IFolder[] = useMemo(() => { + return [{ name: 'View all', pinned: true }, ...foldersData].filter( + (folder: IFolder) => folder.pinned, + ); + }, [foldersData]); + + return ( + <> + <> + File Manager | DesignSparx + + + + + + + + + + + + console.log('accepted files', files)} + onReject={(files) => console.log('rejected files', files)} + maxSize={5 * 1024 ** 2} + > + + + + + + + + + + + +
+ Click or drag and drop your file(s) here +
+
+
+
+ + + Recently modified + + + + {filesData.slice(0, 4).map((file: IFile) => ( + + ))} + + + + + All files + + + ({ + label: folder.name, + value: folder.name, + }))} + /> + } + placeholder="Search" + /> + + file.type === value) + } + error={filesError} + loading={filesLoading} + /> + +
+ + ); +} + +export default FileManager; diff --git a/app/apps/file-manager/types/index.ts b/app/apps/file-manager/types/index.ts new file mode 100644 index 0000000..80c9a9b --- /dev/null +++ b/app/apps/file-manager/types/index.ts @@ -0,0 +1,64 @@ +export type IFileType = + | 'Documents' + | 'Images' + | 'Code' + | 'Spreadsheet' + | 'Presentation' + | 'Text'; + +export type IFile = { + id: string; + name: string; + type: IFileType; + size: string; + created_at: string; + modified_at: string; + owner: string; + path: string; + permissions: string; +}; + +export type IFolderType = + | 'Documents' + | 'Images' + | 'Videos' + | 'Music' + | 'Code' + | 'Downloads' + | 'Backups' + | 'Projects' + | 'Shared' + | 'Trash'; + +export type IFolder = { + id: string; + name: IFolderType; + icon: IFileType; + description: string; + permissions: string; + created_at: string; + pinned?: boolean; + total_files: number; + estimated_size: string; +}; + +export type IFileAction = + | 'Created' + | 'Edited' + | 'Deleted' + | 'Viewed' + | 'Renamed' + | 'Downloaded' + | 'Uploaded' + | 'Shared' + | 'Moved' + | 'Copied'; + +export type IFileActivity = { + id: string; + timestamp: string; + action: IFileAction; + file_name: string; + file_type: IFileType; + user: string; +}; diff --git a/app/apps/file-manager/utils/index.ts b/app/apps/file-manager/utils/index.ts new file mode 100644 index 0000000..ab92adc --- /dev/null +++ b/app/apps/file-manager/utils/index.ts @@ -0,0 +1,87 @@ +import { + IconArchive, + IconArrowDown, + IconArrowsMoveHorizontal, + IconCode, + IconCopy, + IconDownload, + IconEye, + IconFile, + IconFileCode, + IconFilePlus, + IconFileSpreadsheet, + IconFileText, + IconFolder, + IconFolderPlus, + IconMusic, + IconPencil, + IconPhoto, + IconQuestionMark, + IconShare, + IconSlideshow, + IconTrash, + IconUpload, + IconUsers, + IconVideo, +} from '@tabler/icons-react'; + +import { IFileType, IFolderType } from '@/app/apps/file-manager/types'; + +export function resolveFileIcon(fileType: IFileType) { + const iconMap: Record = { + Documents: IconFileText, + Images: IconPhoto, + Code: IconFileCode, + Spreadsheet: IconFileSpreadsheet, + Presentation: IconSlideshow, + Text: IconFile, + }; + + return iconMap[fileType] || IconQuestionMark; // Default icon for unknown types +} + +type FileAction = + | 'Created' + | 'Edited' + | 'Deleted' + | 'Viewed' + | 'Renamed' + | 'Downloaded' + | 'Uploaded' + | 'Shared' + | 'Moved' + | 'Copied'; + +export function resolveActionIcon(action: FileAction) { + const actionIconMap: Record = { + Created: IconFilePlus, + Edited: IconPencil, + Deleted: IconTrash, + Viewed: IconEye, + Renamed: IconFileText, + Downloaded: IconArrowDown, + Uploaded: IconUpload, + Shared: IconShare, + Moved: IconArrowsMoveHorizontal, + Copied: IconCopy, + }; + + return actionIconMap[action] || IconQuestionMark; // Default icon for unknown actions +} + +export function resolveFolderIcon(folderType: IFolderType) { + const folderIconMap: Record = { + Documents: IconFolder, + Images: IconPhoto, + Videos: IconVideo, + Music: IconMusic, + Code: IconCode, + Downloads: IconDownload, + Backups: IconArchive, + Projects: IconFolderPlus, + Shared: IconUsers, + Trash: IconTrash, + }; + + return folderIconMap[folderType] || IconQuestionMark; // Default icon for unknown folder types +} diff --git a/app/error.module.css b/app/error.module.css index ffb9a3f..ffaa966 100644 --- a/app/error.module.css +++ b/app/error.module.css @@ -7,7 +7,7 @@ font-weight: 900; font-size: rem(220px); line-height: 1; - color: light-dark(var(--mantine-color-black), var(--mantine-color-white)); + color: var(--mantine-primary-color-filled); @media (max-width: $mantine-breakpoint-sm) { font-size: rem(120px); diff --git a/app/error.tsx b/app/error.tsx index dbe640e..343ed93 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -1,17 +1,20 @@ 'use client'; import { useEffect } from 'react'; + import { Button, Center, + Code, Group, Stack, Text, Title, useMantineTheme, } from '@mantine/core'; -import { useRouter } from 'next/navigation'; import { IconHome2, IconRefresh } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; + import classes from './error.module.css'; function Error({ @@ -49,9 +52,16 @@ function Error({
400
Sorry, unexpected error.. - + {error.toString()} - +