diff --git a/.changeset/gold-rocks-appear.md b/.changeset/gold-rocks-appear.md new file mode 100644 index 0000000..e75fbb8 --- /dev/null +++ b/.changeset/gold-rocks-appear.md @@ -0,0 +1,5 @@ +--- +"mantine-analytics-dashboard": patch +--- + +feat: added file manager route and update dependencies 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()} - +