build ok,
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
import { ItemCard } from './item-card';
|
||||
import { StorageContext } from './storage-context';
|
||||
|
||||
export function GridView(): React.JSX.Element {
|
||||
const {
|
||||
items,
|
||||
deleteItem: onItemDelete,
|
||||
favoriteItem: onItemFavorite,
|
||||
setCurrentItemId,
|
||||
} = React.useContext(StorageContext);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{ display: 'grid', gap: 4, gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' } }}
|
||||
>
|
||||
{Array.from(items.values()).map((item) => (
|
||||
<ItemCard
|
||||
item={item}
|
||||
key={item.id}
|
||||
onDelete={onItemDelete}
|
||||
onFavorite={onItemFavorite}
|
||||
onOpen={setCurrentItemId}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,119 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import AvatarGroup from '@mui/material/AvatarGroup';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { DotsThree as DotsThreeIcon } from '@phosphor-icons/react/dist/ssr/DotsThree';
|
||||
import { Globe as GlobeIcon } from '@phosphor-icons/react/dist/ssr/Globe';
|
||||
import { Star as StarIcon } from '@phosphor-icons/react/dist/ssr/Star';
|
||||
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { usePopover } from '@/hooks/use-popover';
|
||||
|
||||
import { ItemIcon } from './item-icon';
|
||||
import { ItemMenu } from './item-menu';
|
||||
import type { Item } from './types';
|
||||
|
||||
export interface ItemCardProps {
|
||||
item: Item;
|
||||
onDelete?: (itemId: string) => void;
|
||||
onFavorite?: (itemId: string, value: boolean) => void;
|
||||
onOpen?: (itemId: string) => void;
|
||||
}
|
||||
|
||||
export function ItemCard({ item, onDelete, onFavorite, onOpen }: ItemCardProps): React.JSX.Element {
|
||||
const popover = usePopover<HTMLButtonElement>();
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
popover.handleClose();
|
||||
onDelete?.(item.id);
|
||||
}, [item, popover, onDelete]);
|
||||
|
||||
const createdAt = item.createdAt ? dayjs(item.createdAt).format('MMM D, YYYY') : undefined;
|
||||
const sharedWith = item.shared ?? [];
|
||||
const showShared = !item.isPublic && sharedWith.length > 0;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card
|
||||
key={item.id}
|
||||
sx={{
|
||||
transition: 'box-shadow 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
|
||||
'&:hover': { boxShadow: 'var(--mui-shadows-16)' },
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={3} sx={{ alignItems: 'center', justifyContent: 'space-between', pt: 2, px: 2 }}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onFavorite?.(item.id, !item.isFavorite);
|
||||
}}
|
||||
>
|
||||
<StarIcon color="var(--mui-palette-warning-main)" weight={item.isFavorite ? 'fill' : undefined} />
|
||||
</IconButton>
|
||||
<IconButton onClick={popover.handleOpen} ref={popover.anchorRef}>
|
||||
<DotsThreeIcon weight="bold" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack divider={<Divider />} spacing={1} sx={{ p: 2 }}>
|
||||
<Box
|
||||
onClick={() => {
|
||||
onOpen?.(item.id);
|
||||
}}
|
||||
sx={{ display: 'inline-flex', cursor: 'pointer' }}
|
||||
>
|
||||
<ItemIcon extension={item.extension} type={item.type} />
|
||||
</Box>
|
||||
<div>
|
||||
<Typography
|
||||
onClick={() => {
|
||||
onOpen?.(item.id);
|
||||
}}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{item.size}
|
||||
{item.type === 'folder' ? <span> • {item.itemsCount} items</span> : null}
|
||||
</Typography>
|
||||
<div>
|
||||
{item.isPublic ? (
|
||||
<Tooltip title="Public">
|
||||
<Avatar sx={{ '--Avatar-size': '32px' }}>
|
||||
<GlobeIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{showShared ? (
|
||||
<AvatarGroup max={3}>
|
||||
{sharedWith.map((person) => (
|
||||
<Avatar key={person.name} src={person.avatar} sx={{ '--Avatar-size': '32px' }} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
) : null}
|
||||
</div>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
Created at {createdAt}
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
</Card>
|
||||
<ItemMenu
|
||||
anchorEl={popover.anchorRef.current}
|
||||
onClose={popover.handleClose}
|
||||
onDelete={handleDelete}
|
||||
open={popover.open}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
import { FileIcon } from '@/components/core/file-icon';
|
||||
|
||||
import type { ItemType } from './types';
|
||||
|
||||
export interface ItemIconProps {
|
||||
extension?: string;
|
||||
type: ItemType;
|
||||
}
|
||||
|
||||
export function ItemIcon({ type, extension }: ItemIconProps): React.JSX.Element {
|
||||
return type === 'folder' ? <FolderIcon /> : <FileIcon extension={extension} />;
|
||||
}
|
||||
|
||||
function FolderIcon(): React.JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
display: 'inline-flex',
|
||||
flex: '0 0 auto',
|
||||
justifyContent: 'center',
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
}}
|
||||
>
|
||||
<Box alt="Folder" component="img" src="/assets/icon-folder.svg" sx={{ height: '100%', width: 'auto' }} />
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { Link as LinkIcon } from '@phosphor-icons/react/dist/ssr/Link';
|
||||
import { Trash as TrashIcon } from '@phosphor-icons/react/dist/ssr/Trash';
|
||||
|
||||
export interface ItemMenuProps {
|
||||
anchorEl?: HTMLElement | null;
|
||||
onClose?: () => void;
|
||||
onDelete?: () => void;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
export function ItemMenu({ anchorEl, onClose, onDelete, open = false }: ItemMenuProps): React.JSX.Element {
|
||||
return (
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
>
|
||||
<MenuItem onClick={onClose}>
|
||||
<ListItemIcon>
|
||||
<LinkIcon />
|
||||
</ListItemIcon>
|
||||
Copy Link
|
||||
</MenuItem>
|
||||
<MenuItem onClick={onDelete} sx={{ color: 'var(--mui-palette-error-main)' }}>
|
||||
<ListItemIcon>
|
||||
<TrashIcon />
|
||||
</ListItemIcon>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
}
|
@@ -0,0 +1,220 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import AvatarGroup from '@mui/material/AvatarGroup';
|
||||
import Box from '@mui/material/Box';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { Globe as GlobeIcon } from '@phosphor-icons/react/dist/ssr/Globe';
|
||||
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import { Star as StarIcon } from '@phosphor-icons/react/dist/ssr/Star';
|
||||
import { Trash as TrashIcon } from '@phosphor-icons/react/dist/ssr/Trash';
|
||||
import { X as XIcon } from '@phosphor-icons/react/dist/ssr/X';
|
||||
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { usePopover } from '@/hooks/use-popover';
|
||||
|
||||
import { ItemIcon } from './item-icon';
|
||||
import type { Item } from './types';
|
||||
|
||||
const tagOptions = ['Personal', 'Work', 'Business', 'Accounting', 'Security', 'Design'] satisfies string[];
|
||||
|
||||
export interface ItemModalProps {
|
||||
item: Item;
|
||||
onClose?: () => void;
|
||||
onDelete?: (itemId: string) => void;
|
||||
onFavorite?: (itemId: string, value: boolean) => void;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
export function ItemModal({ item, onClose, onDelete, onFavorite, open = false }: ItemModalProps): React.JSX.Element {
|
||||
const tagsPopover = usePopover<HTMLButtonElement>();
|
||||
|
||||
const tags = item.tags ?? [];
|
||||
const sharedWith = item.shared ?? [];
|
||||
const showShared = !item.isPublic && sharedWith.length > 0;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Dialog
|
||||
maxWidth="sm"
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
sx={{
|
||||
'& .MuiDialog-container': { justifyContent: 'flex-end' },
|
||||
'& .MuiDialog-paper': { height: '100%', width: '100%' },
|
||||
}}
|
||||
>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', minHeight: 0, p: 0 }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
borderBottom: '1px solid var(--mui-palette-divider)',
|
||||
flex: '0 0 auto',
|
||||
justifyContent: 'space-between',
|
||||
p: 3,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onFavorite?.(item.id, !item.isFavorite);
|
||||
}}
|
||||
>
|
||||
<StarIcon color="var(--mui-palette-warning-main)" weight={item.isFavorite ? 'fill' : undefined} />
|
||||
</IconButton>
|
||||
<IconButton onClick={onClose}>
|
||||
<XIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack spacing={2} sx={{ flex: '1 1 auto', minHeight: 0, overflowY: 'auto', px: 3, py: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
border: '1px dashed var(--mui-palette-divider)',
|
||||
borderRadius: 1,
|
||||
display: 'flex',
|
||||
flex: '0 0 auto',
|
||||
justifyContent: 'center',
|
||||
p: 3,
|
||||
}}
|
||||
>
|
||||
<ItemIcon extension={item.extension} type={item.type} />
|
||||
</Box>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="h6">{item.name}</Typography>
|
||||
<IconButton>
|
||||
<PencilSimpleIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<div>
|
||||
<Grid alignItems="center" container spacing={3}>
|
||||
<Grid sm={4} xs={12}>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Created by
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid sm={8} xs={12}>
|
||||
{item.author ? <Avatar src={item.author.avatar} /> : null}
|
||||
</Grid>
|
||||
<Grid sm={4} xs={12}>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Size
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid sm={8} xs={12}>
|
||||
<Typography variant="body2">{item.size}</Typography>
|
||||
</Grid>
|
||||
<Grid sm={4} xs={12}>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Created At
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid sm={8} xs={12}>
|
||||
<Typography variant="body2">
|
||||
{item.createdAt ? dayjs(item.createdAt).format('MMM D, YYYY hh:mm A') : undefined}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid sm={4} xs={12}>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Modified At
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid sm={8} xs={12}>
|
||||
<Typography variant="body2">
|
||||
{item.updatedAt ? dayjs(item.updatedAt).format('MMM D, YYYY hh:mm A') : undefined}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid sm={4} xs={12}>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Tags
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid sm={8} xs={12}>
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
{tags.map((tag) => (
|
||||
<Chip
|
||||
key={tag}
|
||||
label={tag}
|
||||
onDelete={() => {
|
||||
// noop
|
||||
}}
|
||||
size="small"
|
||||
variant="soft"
|
||||
/>
|
||||
))}
|
||||
<IconButton onClick={tagsPopover.handleOpen} ref={tagsPopover.anchorRef}>
|
||||
<PlusIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid sm={4} xs={12}>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Shared with
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid sm={8} xs={12}>
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||
{item.isPublic ? (
|
||||
<Tooltip title="Public">
|
||||
<Avatar sx={{ '--Avatar-size': '32px' }}>
|
||||
<GlobeIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{showShared ? (
|
||||
<AvatarGroup max={3}>
|
||||
{sharedWith.map((person) => (
|
||||
<Avatar key={person.name} src={person.avatar} sx={{ '--Avatar-size': '32px' }} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
) : null}
|
||||
<IconButton>
|
||||
<PlusIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid sm={4} xs={12}>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Actions
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid sm={8} xs={12}>
|
||||
<IconButton
|
||||
color="error"
|
||||
onClick={() => {
|
||||
onDelete?.(item.id);
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Menu
|
||||
anchorEl={tagsPopover.anchorRef.current}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
onClose={tagsPopover.handleClose}
|
||||
open={tagsPopover.open}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
>
|
||||
{tagOptions.map((option) => (
|
||||
<MenuItem key={option}>{option}</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@@ -0,0 +1,147 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import AvatarGroup from '@mui/material/AvatarGroup';
|
||||
import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { DotsThree as DotsThreeIcon } from '@phosphor-icons/react/dist/ssr/DotsThree';
|
||||
import { Globe as GlobeIcon } from '@phosphor-icons/react/dist/ssr/Globe';
|
||||
import { Star as StarIcon } from '@phosphor-icons/react/dist/ssr/Star';
|
||||
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { usePopover } from '@/hooks/use-popover';
|
||||
|
||||
import { ItemIcon } from './item-icon';
|
||||
import { ItemMenu } from './item-menu';
|
||||
import type { Item } from './types';
|
||||
|
||||
export interface ItemRowProps {
|
||||
item: Item;
|
||||
onDelete?: (itemId: string) => void;
|
||||
onFavorite?: (itemId: string, value: boolean) => void;
|
||||
onOpen?: (itemId: string) => void;
|
||||
}
|
||||
|
||||
export function ItemRow({ item, onDelete, onFavorite, onOpen }: ItemRowProps): React.JSX.Element {
|
||||
const popover = usePopover<HTMLButtonElement>();
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
popover.handleClose();
|
||||
onDelete?.(item.id);
|
||||
}, [item, popover, onDelete]);
|
||||
|
||||
const sharedWith = item.shared ?? [];
|
||||
const showShared = !item.isPublic && sharedWith.length > 0;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow
|
||||
key={item.id}
|
||||
sx={{
|
||||
bgcolor: 'var(--mui-palette-background-paper)',
|
||||
borderRadius: 1.5,
|
||||
boxShadow: 0,
|
||||
transition: 'box-shadow 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
|
||||
'&:hover': { boxShadow: 'var(--mui-shadows-16)' },
|
||||
'& .MuiTableCell-root': {
|
||||
borderBottom: '1px solid var(--mui-palette-divider)',
|
||||
borderTop: '1px solid var(--mui-palette-divider)',
|
||||
'&:first-of-type': {
|
||||
borderTopLeftRadius: '12px',
|
||||
borderBottomLeftRadius: '12px',
|
||||
borderLeft: '1px solid var(--mui-palette-divider)',
|
||||
},
|
||||
'&:last-of-type': {
|
||||
borderTopRightRadius: '12px',
|
||||
borderBottomRightRadius: '12px',
|
||||
borderRight: '1px solid var(--mui-palette-divider)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableCell sx={{ maxWidth: '250px' }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Box
|
||||
onClick={() => {
|
||||
onOpen?.(item.id);
|
||||
}}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
<ItemIcon extension={item.extension} type={item.type} />
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
noWrap
|
||||
onClick={() => {
|
||||
onOpen?.(item.id);
|
||||
}}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
variant="subtitle2"
|
||||
>
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{item.size}
|
||||
{item.type === 'folder' ? <span> • {item.itemsCount} items</span> : null}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography noWrap variant="subtitle2">
|
||||
Created at
|
||||
</Typography>
|
||||
{item.createdAt ? (
|
||||
<Typography color="text.secondary" noWrap variant="body2">
|
||||
{dayjs(item.createdAt).format('MMM D, YYYY')}
|
||||
</Typography>
|
||||
) : undefined}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
{item.isPublic ? (
|
||||
<Tooltip title="Public">
|
||||
<Avatar sx={{ '--Avatar-size': '32px' }}>
|
||||
<GlobeIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{showShared ? (
|
||||
<AvatarGroup max={3}>
|
||||
{sharedWith.map((person) => (
|
||||
<Avatar key={person.name} src={person.avatar} sx={{ '--Avatar-size': '32px' }} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
) : null}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onFavorite?.(item.id, !item.isFavorite);
|
||||
}}
|
||||
>
|
||||
<StarIcon color="var(--mui-palette-warning-main)" weight={item.isFavorite ? 'fill' : undefined} />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton onClick={popover.handleOpen} ref={popover.anchorRef}>
|
||||
<DotsThreeIcon weight="bold" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<ItemMenu
|
||||
anchorEl={popover.anchorRef.current || undefined}
|
||||
onClose={popover.handleClose}
|
||||
onDelete={handleDelete}
|
||||
open={popover.open}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@@ -0,0 +1,136 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Card from '@mui/material/Card';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import type { SelectChangeEvent } from '@mui/material/Select';
|
||||
import Select from '@mui/material/Select';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import ToggleButton from '@mui/material/ToggleButton';
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||
import { MagnifyingGlass as MagnifyingGlassIcon } from '@phosphor-icons/react/dist/ssr/MagnifyingGlass';
|
||||
import { Rows as RowsIcon } from '@phosphor-icons/react/dist/ssr/Rows';
|
||||
import { SquaresFour as SquaresFourIcon } from '@phosphor-icons/react/dist/ssr/SquaresFour';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
import { Option } from '@/components/core/option';
|
||||
|
||||
export interface Filters {
|
||||
query?: string;
|
||||
}
|
||||
|
||||
export type ViewMode = 'grid' | 'list';
|
||||
|
||||
export type SortDir = 'asc' | 'desc';
|
||||
|
||||
export interface ItemsFiltersProps {
|
||||
filters?: Filters;
|
||||
sortDir?: SortDir;
|
||||
view?: ViewMode;
|
||||
}
|
||||
|
||||
export function ItemsFilters({ filters = {}, sortDir = 'desc', view = 'grid' }: ItemsFiltersProps): React.JSX.Element {
|
||||
const router = useRouter();
|
||||
|
||||
const [query, setQuery] = React.useState(filters.query ?? '');
|
||||
|
||||
React.useEffect(() => {
|
||||
setQuery(filters.query ?? '');
|
||||
}, [filters]);
|
||||
|
||||
const updateSearchParams = React.useCallback(
|
||||
(newFilters: Filters, newSortDir: SortDir): void => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
// Keep the view mode
|
||||
if (view) {
|
||||
searchParams.set('view', view);
|
||||
}
|
||||
|
||||
if (newSortDir === 'asc') {
|
||||
searchParams.set('sortDir', newSortDir);
|
||||
}
|
||||
|
||||
if (newFilters.query) {
|
||||
searchParams.set('query', newFilters.query);
|
||||
}
|
||||
|
||||
router.push(`${paths.dashboard.fileStorage}?${searchParams.toString()}`);
|
||||
},
|
||||
[router, view]
|
||||
);
|
||||
|
||||
const handleQueryChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setQuery(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleQueryApply = React.useCallback(() => {
|
||||
updateSearchParams({ ...filters, query }, sortDir);
|
||||
}, [updateSearchParams, filters, query, sortDir]);
|
||||
|
||||
const handleSortChange = React.useCallback(
|
||||
(event: SelectChangeEvent) => {
|
||||
updateSearchParams(filters, event.target.value as SortDir);
|
||||
},
|
||||
[updateSearchParams, filters]
|
||||
);
|
||||
|
||||
const handleViewChange = React.useCallback(
|
||||
(value: ViewMode) => {
|
||||
if (value) {
|
||||
router.push(`${paths.dashboard.fileStorage}?view=${value}`);
|
||||
}
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap', p: 2 }}>
|
||||
<OutlinedInput
|
||||
name="name"
|
||||
onChange={handleQueryChange}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleQueryApply();
|
||||
}
|
||||
}}
|
||||
placeholder="Search"
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<MagnifyingGlassIcon fontSize="var(--icon-fontSize-md)" />
|
||||
</InputAdornment>
|
||||
}
|
||||
sx={{ flex: '1 1 auto' }}
|
||||
/>
|
||||
<Select name="sort" onChange={handleSortChange} sx={{ maxWidth: '100%', width: '120px' }} value={sortDir}>
|
||||
<Option value="desc">Newest</Option>
|
||||
<Option value="asc">Oldest</Option>
|
||||
</Select>
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
exclusive
|
||||
onChange={(_, value: ViewMode) => {
|
||||
handleViewChange(value);
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
handleViewChange(view === 'grid' ? 'list' : 'grid');
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
value={view}
|
||||
>
|
||||
<ToggleButton value="grid">
|
||||
<SquaresFourIcon />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="list">
|
||||
<RowsIcon />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import TablePagination from '@mui/material/TablePagination';
|
||||
|
||||
function noop(): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface ItemsPaginationProps {
|
||||
count: number;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export function ItemsPagination({ count, page }: ItemsPaginationProps): React.JSX.Element {
|
||||
return (
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={count}
|
||||
onPageChange={noop}
|
||||
onRowsPerPageChange={noop}
|
||||
page={page}
|
||||
rowsPerPage={10}
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
|
||||
import { ItemRow } from './item-row';
|
||||
import { StorageContext } from './storage-context';
|
||||
|
||||
export function ListView(): React.JSX.Element {
|
||||
const {
|
||||
items,
|
||||
deleteItem: onItemDelete,
|
||||
favoriteItem: onItemFavorite,
|
||||
setCurrentItemId,
|
||||
} = React.useContext(StorageContext);
|
||||
|
||||
return (
|
||||
<Box sx={{ mx: -3, my: -6 }}>
|
||||
<Box sx={{ overflowX: 'auto', px: 3 }}>
|
||||
<Table sx={{ borderCollapse: 'separate', borderSpacing: '0 24px' }}>
|
||||
<TableHead sx={{ visibility: 'collapse' }}>
|
||||
<TableRow>
|
||||
<TableCell sx={{ width: '250px', minWidth: '250px', maxWidth: '250px' }} />
|
||||
<TableCell sx={{ width: '150px', minWidth: '150px', maxWidth: '150px' }} />
|
||||
<TableCell sx={{ width: '150px', minWidth: '150px', maxWidth: '150px' }} />
|
||||
<TableCell sx={{ width: '75px', minWidth: '75px', maxWidth: '75px' }} />
|
||||
<TableCell sx={{ width: '75px', minWidth: '75px', maxWidth: '75px' }} />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Array.from(items.values()).map((item) => (
|
||||
<ItemRow
|
||||
item={item}
|
||||
key={item.id}
|
||||
onDelete={onItemDelete}
|
||||
onFavorite={onItemFavorite}
|
||||
onOpen={setCurrentItemId}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
129
002_source/cms/src/components/dashboard/file-storage/stats.tsx
Normal file
129
002_source/cms/src/components/dashboard/file-storage/stats.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardActions from '@mui/material/CardActions';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Lightning as LightningIcon } from '@phosphor-icons/react/dist/ssr/Lightning';
|
||||
import { RadialBar, RadialBarChart } from 'recharts';
|
||||
|
||||
import { FileIcon } from '@/components/core/file-icon';
|
||||
import { NoSsr } from '@/components/core/no-ssr';
|
||||
|
||||
const totals = [
|
||||
{ extension: 'mp4', itemsCount: 25, label: 'MP4', size: '22.75 GB' },
|
||||
{ extension: 'png', itemsCount: 591, label: 'PNG', size: '54.69 GB' },
|
||||
{ extension: 'pdf', itemsCount: 95, label: 'PDF', size: '412.39 MB' },
|
||||
{ itemsCount: 210, label: 'Other', size: '261.43 MB' },
|
||||
] satisfies { extension?: string; itemsCount: number; label: string; size: string }[];
|
||||
|
||||
export function Stats(): React.JSX.Element {
|
||||
const chartSize = 300;
|
||||
|
||||
const data = [
|
||||
{ name: 'Empty', value: 100 },
|
||||
{ name: 'Usage', value: 75 },
|
||||
] satisfies { name: string; value: number }[];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader subheader="Upgrade before reaching it" title="Stats" />
|
||||
<CardContent>
|
||||
<Stack spacing={2}>
|
||||
<Stack sx={{ alignItems: 'center' }}>
|
||||
<NoSsr fallback={<Box sx={{ height: `${chartSize}px` }} />}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
// hide the empty bar
|
||||
'& .recharts-layer path[name="Empty"]': { display: 'none' },
|
||||
'& .recharts-layer .recharts-radial-bar-background-sector': {
|
||||
fill: 'var(--mui-palette-neutral-100)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RadialBarChart
|
||||
barSize={32}
|
||||
data={data}
|
||||
endAngle={-10}
|
||||
height={chartSize}
|
||||
innerRadius={166}
|
||||
startAngle={190}
|
||||
width={chartSize}
|
||||
>
|
||||
<RadialBar
|
||||
animationDuration={300}
|
||||
background
|
||||
cornerRadius={16}
|
||||
dataKey="value"
|
||||
endAngle={-320}
|
||||
fill="var(--mui-palette-primary-main)"
|
||||
startAngle={20}
|
||||
/>
|
||||
</RadialBarChart>
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ textAlign: 'center', mt: '-50px' }}>
|
||||
<Typography variant="h4">75 GB</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</NoSsr>
|
||||
<Box sx={{ mt: '-100px' }}>
|
||||
<Typography variant="h6">You've almost reached your limit</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
You have used 75% of your available storage.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<List disablePadding>
|
||||
{totals.map((total) => (
|
||||
<ListItem disableGutters key={total.label}>
|
||||
<ListItemIcon>
|
||||
<FileIcon extension={total.extension} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
disableTypography
|
||||
primary={<Typography variant="body2">{total.label}</Typography>}
|
||||
secondary={
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
{total.size} • {total.itemsCount} items
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardActions sx={{ justifyContent: 'flex-end' }}>
|
||||
<Button color="secondary" startIcon={<LightningIcon />} variant="contained">
|
||||
Upgrade plan
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import type { Item } from './types';
|
||||
|
||||
function noop(): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export interface StorageContextValue {
|
||||
items: Map<string, Item>;
|
||||
currentItemId?: string;
|
||||
setCurrentItemId: (itemId?: string) => void;
|
||||
deleteItem: (itemId: string) => void;
|
||||
favoriteItem: (itemId: string, value: boolean) => void;
|
||||
}
|
||||
|
||||
export const StorageContext = React.createContext<StorageContextValue>({
|
||||
items: new Map(),
|
||||
setCurrentItemId: noop,
|
||||
deleteItem: noop,
|
||||
favoriteItem: noop,
|
||||
});
|
||||
|
||||
export interface StorageProviderProps {
|
||||
children: React.ReactNode;
|
||||
items: Item[];
|
||||
}
|
||||
|
||||
export function StorageProvider({ children, items: initialItems = [] }: StorageProviderProps): React.JSX.Element {
|
||||
const [items, setItems] = React.useState(new Map<string, Item>());
|
||||
const [currentItemId, setCurrentItemId] = React.useState<string>();
|
||||
|
||||
React.useEffect((): void => {
|
||||
setItems(new Map(initialItems.map((item) => [item.id, item])));
|
||||
}, [initialItems]);
|
||||
|
||||
const handleDeleteItem = React.useCallback(
|
||||
(itemId: string) => {
|
||||
const item = items.get(itemId);
|
||||
|
||||
// Item might no longer exist
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedItems = new Map<string, Item>(items);
|
||||
|
||||
// Delete item
|
||||
updatedItems.delete(itemId);
|
||||
|
||||
// Dispatch update
|
||||
setItems(updatedItems);
|
||||
},
|
||||
[items]
|
||||
);
|
||||
|
||||
const handleFavoriteItem = React.useCallback(
|
||||
(itemId: string, value: boolean) => {
|
||||
const item = items.get(itemId);
|
||||
|
||||
// Item might no longer exist
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedItems = new Map<string, Item>(items);
|
||||
|
||||
// Update item
|
||||
updatedItems.set(itemId, { ...item, isFavorite: value });
|
||||
|
||||
// Dispatch update
|
||||
setItems(updatedItems);
|
||||
},
|
||||
[items]
|
||||
);
|
||||
|
||||
return (
|
||||
<StorageContext.Provider
|
||||
value={{ items, currentItemId, setCurrentItemId, deleteItem: handleDeleteItem, favoriteItem: handleFavoriteItem }}
|
||||
>
|
||||
{children}
|
||||
</StorageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const StorageConsumer = StorageContext.Consumer;
|
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import { GridView } from './grid-view';
|
||||
import { ItemModal } from './item-modal';
|
||||
import { ListView } from './list-view';
|
||||
import { StorageContext } from './storage-context';
|
||||
|
||||
export interface StorageViewProps {
|
||||
view: 'grid' | 'list';
|
||||
}
|
||||
|
||||
export function StorageView({ view }: StorageViewProps): React.JSX.Element {
|
||||
const { currentItemId, items, deleteItem, favoriteItem, setCurrentItemId } = React.useContext(StorageContext);
|
||||
|
||||
const currentItem = currentItemId ? items.get(currentItemId) : undefined;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{items.size ? (
|
||||
<React.Fragment>{view === 'grid' ? <GridView /> : <ListView />}</React.Fragment>
|
||||
) : (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography color="text.secondary" sx={{ textAlign: 'center' }} variant="body2">
|
||||
No items found
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{currentItem ? (
|
||||
<ItemModal
|
||||
item={currentItem}
|
||||
onClose={() => {
|
||||
setCurrentItemId(undefined);
|
||||
}}
|
||||
onDelete={(itemId) => {
|
||||
setCurrentItemId(undefined);
|
||||
deleteItem(itemId);
|
||||
}}
|
||||
onFavorite={favoriteItem}
|
||||
open
|
||||
/>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
17
002_source/cms/src/components/dashboard/file-storage/types.d.ts
vendored
Normal file
17
002_source/cms/src/components/dashboard/file-storage/types.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
export type ItemType = 'file' | 'folder';
|
||||
|
||||
export interface Item {
|
||||
id: string;
|
||||
type: ItemType;
|
||||
name: string;
|
||||
extension?: string;
|
||||
author?: { name: string; avatar?: string };
|
||||
isFavorite?: boolean;
|
||||
isPublic?: boolean;
|
||||
tags: string[];
|
||||
shared: { name: string; avatar?: string }[];
|
||||
itemsCount?: number;
|
||||
size: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import { UploadSimple as UploadSimpleIcon } from '@phosphor-icons/react/dist/ssr/UploadSimple';
|
||||
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
|
||||
import { Uploader } from './uploader';
|
||||
|
||||
export function UplaodButton(): React.JSX.Element {
|
||||
const uploadDialog = useDialog();
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button onClick={uploadDialog.handleOpen} startIcon={<UploadSimpleIcon />} variant="contained">
|
||||
Upload
|
||||
</Button>
|
||||
<Uploader onClose={uploadDialog.handleClose} open={uploadDialog.open} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { X as XIcon } from '@phosphor-icons/react/dist/ssr/X';
|
||||
|
||||
import type { File } from '@/components/core/file-dropzone';
|
||||
import { FileDropzone } from '@/components/core/file-dropzone';
|
||||
import { FileIcon } from '@/components/core/file-icon';
|
||||
|
||||
function bytesToSize(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) {
|
||||
return '0 Bytes';
|
||||
}
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
export interface UploaderProps {
|
||||
onClose?: () => void;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
export function Uploader({ onClose, open = false }: UploaderProps): React.JSX.Element {
|
||||
const [files, setFiles] = React.useState<File[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFiles([]);
|
||||
}, [open]);
|
||||
|
||||
const handleDrop = React.useCallback((newFiles: File[]) => {
|
||||
setFiles((prevFiles) => {
|
||||
return [...prevFiles, ...newFiles];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleRemove = React.useCallback((file: File) => {
|
||||
setFiles((prevFiles) => {
|
||||
return prevFiles.filter((_file) => _file.path !== file.path);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleRemoveAll = React.useCallback(() => {
|
||||
setFiles([]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog fullWidth maxWidth="sm" onClose={onClose} open={open}>
|
||||
<Stack direction="row" spacing={3} sx={{ alignItems: 'center', justifyContent: 'space-between', px: 3, py: 2 }}>
|
||||
<Typography variant="h6">Upload files</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<XIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<DialogContent>
|
||||
<Stack spacing={3}>
|
||||
<FileDropzone accept={{ '*/*': [] }} caption="Max file size is 3 MB" files={files} onDrop={handleDrop} />
|
||||
{files.length ? (
|
||||
<Stack spacing={2}>
|
||||
<Stack component="ul" spacing={1} sx={{ listStyle: 'none', m: 0, p: 0 }}>
|
||||
{files.map((file) => {
|
||||
const extension = file.name.split('.').pop();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
component="li"
|
||||
direction="row"
|
||||
key={file.path}
|
||||
spacing={2}
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
border: '1px solid var(--mui-palette-divider)',
|
||||
borderRadius: 1,
|
||||
flex: '1 1 auto',
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<FileIcon extension={extension} />
|
||||
<Box sx={{ flex: '1 1 auto' }}>
|
||||
<Typography variant="subtitle2">{file.name}</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{bytesToSize(file.size)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Tooltip title="Remove">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleRemove(file);
|
||||
}}
|
||||
>
|
||||
<XIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||
<Button color="secondary" onClick={handleRemoveAll} size="small" type="button">
|
||||
Remove all
|
||||
</Button>
|
||||
<Button onClick={onClose} size="small" type="button" variant="contained">
|
||||
Upload
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : null}
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user