fix: clean folder
This commit is contained in:
@@ -1,131 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import ReactPlayer from '@/libs/ReactPlayer'
|
||||
|
||||
// Type Imports
|
||||
import type { CourseDetails } from '@/types/apps/academyTypes'
|
||||
|
||||
// Components Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import CustomIconButton from '@core/components/mui/IconButton'
|
||||
|
||||
const Details = ({ data }: { data?: CourseDetails }) => {
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'))
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className='flex flex-wrap items-center justify-between gap-4'>
|
||||
<div>
|
||||
<Typography variant='h5'>UI/UX Basic Fundamentals</Typography>
|
||||
<Typography>
|
||||
Prof. <span className='font-medium text-textPrimary'>Devonne Wallbridge</span>
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Chip label='UI/UX' variant='tonal' size='small' color='error' />
|
||||
<i className='tabler-share cursor-pointer' />
|
||||
<i className='tabler-bookmarks cursor-pointer' />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<div className='border rounded'>
|
||||
<div className='mli-2 mbs-2 overflow-hidden rounded'>
|
||||
<ReactPlayer
|
||||
playing
|
||||
controls
|
||||
url='https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4'
|
||||
height={smallScreen ? 280 : 440}
|
||||
className='bg-black !is-full'
|
||||
light={
|
||||
<img
|
||||
src='/images/apps/academy/4.png'
|
||||
alt='Thumbnail'
|
||||
className='is-full bs-full object-cover bg-backgroundPaper'
|
||||
/>
|
||||
}
|
||||
playIcon={
|
||||
<CustomIconButton variant='contained' color='error' className='absolute rounded-full'>
|
||||
<i className='tabler-player-play text-2xl' />
|
||||
</CustomIconButton>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-6 p-5'>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Typography variant='h5'>About this course</Typography>
|
||||
<Typography>{data?.about}</Typography>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Typography variant='h5'>By the numbers</Typography>
|
||||
<div className='flex flex-wrap gap-x-12 gap-y-2'>
|
||||
<List role='list' component='div' className='flex flex-col gap-2 plb-0'>
|
||||
<ListItem role='listitem' className='flex items-center gap-2 p-0'>
|
||||
<i className='tabler-check text-xl text-textSecondary' />
|
||||
<Typography>Skill level: {data?.skillLevel}</Typography>
|
||||
</ListItem>
|
||||
<ListItem role='listitem' className='flex items-center gap-2 p-0'>
|
||||
<i className='tabler-users text-xl text-textSecondary' />
|
||||
<Typography>Students: {data?.totalStudents.toLocaleString()}</Typography>
|
||||
</ListItem>
|
||||
<ListItem role='listitem' className='flex items-center gap-2 p-0'>
|
||||
<i className='tabler-world text-xl text-textSecondary' />
|
||||
<Typography>Languages: {data?.language}</Typography>
|
||||
</ListItem>
|
||||
<ListItem role='listitem' className='flex items-center gap-2 p-0'>
|
||||
<i className='tabler-file text-xl text-textSecondary' />
|
||||
<Typography>Captions: {data?.isCaptions ? 'Yes' : 'No'}</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
<List role='list' component='div' className='flex flex-col gap-2 plb-0'>
|
||||
<ListItem role='listitem' className='flex items-center gap-2 p-0'>
|
||||
<i className='tabler-video text-xl text-textSecondary' />
|
||||
<Typography>Lectures: {data?.totalLectures}</Typography>
|
||||
</ListItem>
|
||||
<ListItem role='listitem' className='flex items-center gap-2 p-0'>
|
||||
<i className='tabler-clock text-xl text-textSecondary' />
|
||||
<Typography>Video: {data?.length}</Typography>
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Typography variant='h5'>Description</Typography>
|
||||
{data?.description.map((value, index) => <Typography key={index}>{value}</Typography>)}
|
||||
</div>
|
||||
<Divider />
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Typography variant='h5'>Instructor</Typography>
|
||||
<div className='flex items-center gap-4'>
|
||||
<CustomAvatar skin='light-static' color='error' src={data?.instructorAvatar} size={38} />
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
{data?.instructor}
|
||||
</Typography>
|
||||
<Typography variant='body2'>{data?.instructorPosition}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default Details
|
||||
@@ -1,151 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { ChangeEvent, SyntheticEvent } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import { styled } from '@mui/material/styles'
|
||||
import MuiAccordion from '@mui/material/Accordion'
|
||||
import MuiAccordionSummary from '@mui/material/AccordionSummary'
|
||||
import MuiAccordionDetails from '@mui/material/AccordionDetails'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import List from '@mui/material/List'
|
||||
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||
import type { AccordionProps } from '@mui/material/Accordion'
|
||||
import type { AccordionSummaryProps } from '@mui/material/AccordionSummary'
|
||||
import type { AccordionDetailsProps } from '@mui/material/AccordionDetails'
|
||||
|
||||
// Type Imports
|
||||
import type { CourseContent } from '@/types/apps/academyTypes'
|
||||
|
||||
type ItemsType = {
|
||||
title: string
|
||||
time: string
|
||||
isCompleted: boolean
|
||||
}[]
|
||||
|
||||
// Styled component for Accordion component
|
||||
export const Accordion = styled(MuiAccordion)<AccordionProps>({
|
||||
margin: '0 !important',
|
||||
boxShadow: 'none !important',
|
||||
border: '1px solid var(--mui-palette-divider) !important',
|
||||
borderRadius: '0 !important',
|
||||
overflow: 'hidden',
|
||||
background: 'none',
|
||||
'&:not(:last-of-type)': {
|
||||
borderBottom: '0 !important'
|
||||
},
|
||||
'&:before': {
|
||||
display: 'none'
|
||||
},
|
||||
'&:first-of-type': {
|
||||
borderTopLeftRadius: 'var(--mui-shape-borderRadius) !important',
|
||||
borderTopRightRadius: 'var(--mui-shape-borderRadius) !important'
|
||||
},
|
||||
'&:last-of-type': {
|
||||
borderBottomLeftRadius: 'var(--mui-shape-borderRadius) !important',
|
||||
borderBottomRightRadius: 'var(--mui-shape-borderRadius) !important'
|
||||
}
|
||||
})
|
||||
|
||||
// Styled component for AccordionSummary component
|
||||
export const AccordionSummary = styled(MuiAccordionSummary)<AccordionSummaryProps>(({ theme }) => ({
|
||||
padding: theme.spacing(3, 6),
|
||||
transition: 'none',
|
||||
backgroundColor: 'var(--mui-palette-action-hover)',
|
||||
borderBlockEnd: '0 !important',
|
||||
'&.Mui-expanded': {
|
||||
borderBlockEnd: '1px solid var(--mui-palette-divider) !important'
|
||||
}
|
||||
}))
|
||||
|
||||
// Styled component for AccordionDetails component
|
||||
export const AccordionDetails = styled(MuiAccordionDetails)<AccordionDetailsProps>(({ theme }) => ({
|
||||
padding: `${theme.spacing(4, 3)} !important`,
|
||||
backgroundColor: 'var(--mui-palette-background-paper)'
|
||||
}))
|
||||
|
||||
const Sidebar = ({ content }: { content?: CourseContent[] }) => {
|
||||
// States
|
||||
const [expanded, setExpanded] = useState<number | false>(0)
|
||||
const [items, setItems] = useState<ItemsType[]>(content?.map(item => item.topics) ?? [])
|
||||
|
||||
const handleChange = (panel: number) => (event: SyntheticEvent, isExpanded: boolean) => {
|
||||
setExpanded(isExpanded ? panel : false)
|
||||
}
|
||||
|
||||
const handleCheckboxChange = (e: ChangeEvent<HTMLInputElement>, index1: number, index2: number) => {
|
||||
setItems(
|
||||
items.map((item, i) => {
|
||||
if (i === index1) {
|
||||
return item.map((topic, j) => {
|
||||
if (j === index2) {
|
||||
return { ...topic, isCompleted: e.target.checked }
|
||||
}
|
||||
|
||||
return topic
|
||||
})
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{content?.map((item, index) => {
|
||||
const totalTime = items[index]
|
||||
.reduce((sum, topic) => {
|
||||
const time = parseFloat(topic.time || '0')
|
||||
|
||||
return sum + time
|
||||
}, 0)
|
||||
.toFixed(2)
|
||||
|
||||
const selectedTopics = items[index].filter(topic => topic.isCompleted).length
|
||||
|
||||
return (
|
||||
<Accordion key={index} expanded={expanded === index} onChange={handleChange(index)}>
|
||||
<AccordionSummary
|
||||
id='customized-panel-header-1'
|
||||
expandIcon={<i className='tabler-chevron-right text-textSecondary' />}
|
||||
aria-controls={'sd'}
|
||||
>
|
||||
<div>
|
||||
<Typography variant='h5'>{item.title}</Typography>
|
||||
<Typography className='!font-normal !text-textSecondary'>{`${selectedTopics} / ${item.topics.length} | ${parseFloat(totalTime)} min`}</Typography>
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<List role='list' component='div' className='flex flex-col gap-4 plb-0'>
|
||||
{item.topics.map((topic, i) => {
|
||||
return (
|
||||
<ListItem key={i} role='listitem' className='gap-3 p-0'>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
tabIndex={-1}
|
||||
checked={items[index][i].isCompleted}
|
||||
onChange={e => handleCheckboxChange(e, index, i)}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<div>
|
||||
<Typography className='font-medium !text-textPrimary'>{`${i + 1}. ${topic.title}`}</Typography>
|
||||
<Typography variant='body2'>{topic.time}</Typography>
|
||||
</div>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sidebar
|
||||
@@ -1,76 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
|
||||
// Type Imports
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Components Imports
|
||||
import CustomIconButton from '@core/components/mui/IconButton'
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
import DirectionalIcon from '@components/DirectionalIcon'
|
||||
|
||||
type DataType = {
|
||||
title: string
|
||||
tasks: number
|
||||
progress: number
|
||||
color: ThemeColor
|
||||
}
|
||||
|
||||
// Vars
|
||||
const data: DataType[] = [
|
||||
{ title: 'User Experience Design', tasks: 120, progress: 72, color: 'primary' },
|
||||
{ title: 'Basic fundamentals', tasks: 32, progress: 48, color: 'success' },
|
||||
{ title: 'React Native components', tasks: 182, progress: 15, color: 'error' },
|
||||
{ title: 'Basic of music theory', tasks: 56, progress: 24, color: 'info' }
|
||||
]
|
||||
|
||||
const AssignmentProgress = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Assignment Progress' action={<OptionMenu options={['Refresh', 'Update', 'Share']} />} />
|
||||
<CardContent className='flex flex-col gap-8'>
|
||||
{data.map((item, i) => (
|
||||
<div key={i} className='flex items-center gap-4'>
|
||||
<div className='relative flex items-center justify-center'>
|
||||
<CircularProgress
|
||||
variant='determinate'
|
||||
size={54}
|
||||
value={100}
|
||||
thickness={3}
|
||||
className='absolute text-[var(--mui-palette-customColors-trackBg)]'
|
||||
/>
|
||||
<CircularProgress
|
||||
variant='determinate'
|
||||
size={54}
|
||||
value={item.progress}
|
||||
thickness={3}
|
||||
color={item.color}
|
||||
sx={{ '& .MuiCircularProgress-circle': { strokeLinecap: 'round' } }}
|
||||
/>
|
||||
<Typography className='absolute font-medium' color='text.primary'>
|
||||
{`${item.progress}%`}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex justify-between items-center is-full gap-4'>
|
||||
<div>
|
||||
<Typography className='font-medium mbe-1.5' color='text.primary'>
|
||||
{item.title}
|
||||
</Typography>
|
||||
<Typography variant='body2'>{`${item.tasks} Tasks`}</Typography>
|
||||
</div>
|
||||
<CustomIconButton size='small' variant='tonal' color='secondary' className='min-is-fit'>
|
||||
<DirectionalIcon ltrIconClass='tabler-chevron-right' rtlIconClass='tabler-chevron-left' />
|
||||
</CustomIconButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssignmentProgress
|
||||
@@ -1,353 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import LinearProgress from '@mui/material/LinearProgress'
|
||||
import TablePagination from '@mui/material/TablePagination'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import type { TextFieldProps } from '@mui/material/TextField'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import { rankItem } from '@tanstack/match-sorter-utils'
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
getFilteredRowModel,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFacetedMinMaxValues,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel
|
||||
} from '@tanstack/react-table'
|
||||
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
|
||||
import type { RankingInfo } from '@tanstack/match-sorter-utils'
|
||||
|
||||
// Type Imports
|
||||
import type { Course } from '@/types/apps/academyTypes'
|
||||
import type { Locale } from '@configs/i18n'
|
||||
|
||||
// Components Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import TablePaginationComponent from '@components/TablePaginationComponent'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Util Imports
|
||||
import { getLocalizedUrl } from '@/utils/i18n'
|
||||
|
||||
// Style Imports
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface FilterFns {
|
||||
fuzzy: FilterFn<unknown>
|
||||
}
|
||||
interface FilterMeta {
|
||||
itemRank: RankingInfo
|
||||
}
|
||||
}
|
||||
|
||||
type CourseWithProgress = Course & {
|
||||
progressValue?: string
|
||||
}
|
||||
|
||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value)
|
||||
|
||||
// Store the itemRank info
|
||||
addMeta({
|
||||
itemRank
|
||||
})
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed
|
||||
}
|
||||
|
||||
const DebouncedInput = ({
|
||||
value: initialValue,
|
||||
onChange,
|
||||
debounce = 500,
|
||||
...props
|
||||
}: {
|
||||
value: string | number
|
||||
onChange: (value: string | number) => void
|
||||
debounce?: number
|
||||
} & Omit<TextFieldProps, 'onChange'>) => {
|
||||
// States
|
||||
const [value, setValue] = useState(initialValue)
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue)
|
||||
}, [initialValue])
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
onChange(value)
|
||||
}, debounce)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value])
|
||||
|
||||
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
|
||||
}
|
||||
|
||||
// Column Definitions
|
||||
const columnHelper = createColumnHelper<CourseWithProgress>()
|
||||
|
||||
const CourseTable = ({ courseData }: { courseData?: Course[] }) => {
|
||||
// States
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [data, setData] = useState(...[courseData])
|
||||
const [globalFilter, setGlobalFilter] = useState('')
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
const columns = useMemo<ColumnDef<CourseWithProgress, any>[]>(
|
||||
() => [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
{...{
|
||||
checked: table.getIsAllRowsSelected(),
|
||||
indeterminate: table.getIsSomeRowsSelected(),
|
||||
onChange: table.getToggleAllRowsSelectedHandler()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
{...{
|
||||
checked: row.getIsSelected(),
|
||||
disabled: !row.getCanSelect(),
|
||||
indeterminate: row.getIsSomeSelected(),
|
||||
onChange: row.getToggleSelectedHandler()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
columnHelper.accessor('courseTitle', {
|
||||
header: 'Course Name',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center gap-4'>
|
||||
<CustomAvatar variant='rounded' skin='light' color={row.original.color}>
|
||||
<i className={classnames('text-[28px]', row.original.logo)} />
|
||||
</CustomAvatar>
|
||||
<div className='flex flex-col'>
|
||||
<Typography
|
||||
component={Link}
|
||||
href={getLocalizedUrl('/apps/academy/course-details', locale as Locale)}
|
||||
className='font-medium hover:text-primary'
|
||||
color='text.primary'
|
||||
>
|
||||
{row.original.courseTitle}
|
||||
</Typography>
|
||||
<div className='flex items-center gap-2'>
|
||||
<CustomAvatar src={row.original.image} size={22} />
|
||||
<Typography variant='body2' color='text.primary'>
|
||||
{row.original.user}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('time', {
|
||||
header: 'Time',
|
||||
cell: ({ row }) => (
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
{row.original.time}
|
||||
</Typography>
|
||||
),
|
||||
enableSorting: false
|
||||
}),
|
||||
columnHelper.accessor('progressValue', {
|
||||
header: 'progress',
|
||||
sortingFn: (rowA, rowB) => {
|
||||
if (
|
||||
!Math.floor((rowA.original.completedTasks / rowA.original.totalTasks) * 100) ||
|
||||
!Math.floor((rowB.original.completedTasks / rowB.original.totalTasks) * 100)
|
||||
)
|
||||
return 0
|
||||
|
||||
return (
|
||||
Number(Math.floor((rowA.original.completedTasks / rowA.original.totalTasks) * 100)) -
|
||||
Number(Math.floor((rowB.original.completedTasks / rowB.original.totalTasks) * 100))
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center gap-4 min-is-48'>
|
||||
<Typography
|
||||
className='font-medium'
|
||||
color='text.primary'
|
||||
>{`${Math.floor((row.original.completedTasks / row.original.totalTasks) * 100)}%`}</Typography>
|
||||
<LinearProgress
|
||||
color='primary'
|
||||
value={Math.floor((row.original.completedTasks / row.original.totalTasks) * 100)}
|
||||
variant='determinate'
|
||||
className='is-full bs-2'
|
||||
/>
|
||||
<Typography variant='body2'>{`${row.original.completedTasks}/${row.original.totalTasks}`}</Typography>
|
||||
</div>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('userCount', {
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center justify-between gap-5'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<i className='tabler-users text-primary' />
|
||||
<Typography>{row.original.userCount}</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<i className='tabler-book text-info' />
|
||||
<Typography>{row.original.note}</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<i className='tabler-video text-error' />
|
||||
<Typography>{row.original.view}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false
|
||||
})
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data: data as Course[],
|
||||
columns,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter
|
||||
},
|
||||
state: {
|
||||
rowSelection,
|
||||
globalFilter
|
||||
},
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: 5
|
||||
}
|
||||
},
|
||||
enableRowSelection: true, //enable row selection for all rows
|
||||
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
|
||||
globalFilterFn: fuzzyFilter,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
getFacetedMinMaxValues: getFacetedMinMaxValues()
|
||||
})
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title='Course you are taking'
|
||||
action={
|
||||
<DebouncedInput
|
||||
value={globalFilter ?? ''}
|
||||
onChange={value => setGlobalFilter(String(value))}
|
||||
placeholder='Search Course'
|
||||
/>
|
||||
}
|
||||
className='flex-wrap gap-4'
|
||||
/>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className={tableStyles.table}>
|
||||
<thead>
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<th key={header.id}>
|
||||
{header.isPlaceholder ? null : (
|
||||
<>
|
||||
<div
|
||||
className={classnames({
|
||||
'flex items-center': header.column.getIsSorted(),
|
||||
'cursor-pointer select-none': header.column.getCanSort()
|
||||
})}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{{
|
||||
asc: <i className='tabler-chevron-up text-xl' />,
|
||||
desc: <i className='tabler-chevron-down text-xl' />
|
||||
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
{table.getFilteredRowModel().rows.length === 0 ? (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
||||
No data available
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
) : (
|
||||
<tbody>
|
||||
{table
|
||||
.getRowModel()
|
||||
.rows.slice(0, table.getState().pagination.pageSize)
|
||||
.map(row => {
|
||||
return (
|
||||
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
<TablePagination
|
||||
component={() => (
|
||||
<TablePaginationComponent
|
||||
pageIndex={table.getState().pagination.pageIndex + 1}
|
||||
pageSize={table.getState().pagination.pageSize}
|
||||
totalCount={table.getFilteredRowModel().rows.length}
|
||||
onPageChange={(_, page) => {
|
||||
table.setPageIndex(page - 1)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
count={table.getFilteredRowModel().rows.length}
|
||||
rowsPerPage={table.getState().pagination.pageSize}
|
||||
page={table.getState().pagination.pageIndex}
|
||||
onPageChange={(_, page) => {
|
||||
table.setPageIndex(page)
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default CourseTable
|
||||
@@ -1,193 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// Next Imports
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import type { ApexOptions } from 'apexcharts'
|
||||
|
||||
// Components Imports
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
// Styled Component Imports
|
||||
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
|
||||
|
||||
type DataType = {
|
||||
title: string
|
||||
value: number
|
||||
colorClass: string
|
||||
}
|
||||
|
||||
// Vars
|
||||
const series = [
|
||||
{
|
||||
data: [35, 20, 14, 12, 10, 9]
|
||||
}
|
||||
]
|
||||
|
||||
const data1: DataType[] = [
|
||||
{ title: 'UI Design', value: 35, colorClass: 'text-primary' },
|
||||
{ title: 'UX Design', value: 20, colorClass: 'text-info' },
|
||||
{ title: 'Music', value: 14, colorClass: 'text-success' }
|
||||
]
|
||||
|
||||
const data2: DataType[] = [
|
||||
{ title: 'Animation', value: 12, colorClass: 'text-secondary' },
|
||||
{ title: 'React', value: 10, colorClass: 'text-error' },
|
||||
{ title: 'SEO', value: 9, colorClass: 'text-warning' }
|
||||
]
|
||||
|
||||
const labels = ['UI Design', 'UX Design', 'Music', 'Animation', 'React', 'SEO']
|
||||
|
||||
const InterestedTopics = () => {
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
|
||||
// Vars
|
||||
const options: ApexOptions = {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false }
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
barHeight: '70%',
|
||||
distributed: true,
|
||||
borderRadius: 7,
|
||||
borderRadiusApplication: 'end'
|
||||
}
|
||||
},
|
||||
|
||||
colors: [
|
||||
'var(--mui-palette-primary-main)',
|
||||
'var(--mui-palette-info-main)',
|
||||
'var(--mui-palette-success-main)',
|
||||
'var(--mui-palette-secondary-main)',
|
||||
'var(--mui-palette-error-main)',
|
||||
'var(--mui-palette-warning-main)'
|
||||
],
|
||||
grid: {
|
||||
strokeDashArray: 8,
|
||||
borderColor: 'var(--mui-palette-divider)',
|
||||
xaxis: {
|
||||
lines: { show: true }
|
||||
},
|
||||
yaxis: {
|
||||
lines: { show: false }
|
||||
},
|
||||
padding: {
|
||||
top: -25,
|
||||
left: 21,
|
||||
right: 25,
|
||||
bottom: 0
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
offsetY: 8,
|
||||
style: {
|
||||
colors: ['#fff'],
|
||||
fontWeight: 500,
|
||||
fontSize: '0.8125rem'
|
||||
},
|
||||
formatter(val: string, opt: any) {
|
||||
return labels[opt.dataPointIndex]
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
style: {
|
||||
fontSize: '0.75rem'
|
||||
},
|
||||
onDatasetHover: {
|
||||
highlightDataSeries: false
|
||||
}
|
||||
},
|
||||
legend: { show: false },
|
||||
states: {
|
||||
hover: {
|
||||
filter: { type: 'none' }
|
||||
},
|
||||
active: {
|
||||
filter: { type: 'none' }
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
axisTicks: { show: false },
|
||||
axisBorder: { show: false },
|
||||
categories: ['6', '5', '4', '3', '2', '1'],
|
||||
labels: {
|
||||
formatter: val => `${val}%`,
|
||||
style: {
|
||||
fontSize: '0.8125rem',
|
||||
colors: 'var(--mui-palette-text-disabled)'
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
align: theme.direction === 'rtl' ? 'right' : 'left',
|
||||
style: {
|
||||
fontWeight: 500,
|
||||
fontSize: '0.8125rem',
|
||||
colors: 'var(--mui-palette-text-disabled)'
|
||||
},
|
||||
offsetX: theme.direction === 'rtl' ? -15 : -30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title='Topic you are interested in'
|
||||
action={<OptionMenu options={['Refresh', 'Update', 'Share']} />}
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container>
|
||||
<Grid size={{ xs: 12, sm: 6 }} className='max-sm:mbe-6'>
|
||||
<AppReactApexCharts type='bar' height={296} width='100%' series={series} options={options} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }} alignSelf='center'>
|
||||
<div className='flex justify-around items-start'>
|
||||
<div className='flex flex-col gap-y-12'>
|
||||
{data1.map((item, i) => (
|
||||
<div key={i} className='flex gap-2'>
|
||||
<i className={classnames('tabler-circle-filled text-xs m-[5px]', item.colorClass)} />
|
||||
<div>
|
||||
<Typography>{item.title}</Typography>
|
||||
<Typography variant='h5'>{`${item.value}%`}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex flex-col gap-y-12'>
|
||||
{data2.map((item, i) => (
|
||||
<div key={i} className='flex gap-2'>
|
||||
<i className={classnames('tabler-circle-filled text-xs m-[5px]', item.colorClass)} />
|
||||
<div>
|
||||
<Typography>{item.title}</Typography>
|
||||
<Typography variant='h5'>{`${item.value}%`}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default InterestedTopics
|
||||
@@ -1,59 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Components Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
type DataType = {
|
||||
name: string
|
||||
profession: string
|
||||
totalCourses: number
|
||||
avatar: string
|
||||
}
|
||||
|
||||
// Vars
|
||||
const data: DataType[] = [
|
||||
{ name: 'Jordan Stevenson', profession: 'Business Intelligence', totalCourses: 33, avatar: '/images/avatars/1.png' },
|
||||
{ name: 'Bentlee Emblin', profession: 'Digital Marketing', totalCourses: 52, avatar: '/images/avatars/2.png' },
|
||||
{ name: 'Benedetto Rossiter', profession: 'UI/UX Design', totalCourses: 12, avatar: '/images/avatars/3.png' },
|
||||
{ name: 'Beverlie Krabbe', profession: 'Vue', totalCourses: 8, avatar: '/images/avatars/4.png' }
|
||||
]
|
||||
|
||||
const PopularInstructors = () => {
|
||||
return (
|
||||
<Card className='bs-full'>
|
||||
<CardHeader title='Popular Instructors' action={<OptionMenu options={['Refresh', 'Update', 'Share']} />} />
|
||||
<Divider />
|
||||
<div className='flex justify-between plb-4 pli-6'>
|
||||
<Typography className='uppercase'>instructors</Typography>
|
||||
<Typography className='uppercase'>courses</Typography>
|
||||
</div>
|
||||
<Divider />
|
||||
<CardContent className='flex flex-col gap-4'>
|
||||
{data.map((item, i) => (
|
||||
<div key={i} className='flex items-center gap-4'>
|
||||
<CustomAvatar size={34} src={item.avatar} />
|
||||
<div className='flex justify-between items-center is-full gap-4'>
|
||||
<div>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Typography variant='body2'>{item.profession}</Typography>
|
||||
</div>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
{item.totalCourses}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default PopularInstructors
|
||||
@@ -1,54 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Chip from '@mui/material/Chip'
|
||||
|
||||
// Type Imports
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Components Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
type DataType = {
|
||||
title: string
|
||||
views: string
|
||||
icon: string
|
||||
color: ThemeColor
|
||||
}
|
||||
|
||||
// Vars
|
||||
const data: DataType[] = [
|
||||
{ title: 'Videography Basic Design Course', views: '1.2k', icon: 'tabler-video', color: 'primary' },
|
||||
{ title: 'Basic Front-end Development Course', views: '834', icon: 'tabler-code', color: 'info' },
|
||||
{ title: 'Basic Fundamentals of Photography', views: '3.7k', icon: 'tabler-camera', color: 'success' },
|
||||
{ title: 'Advance Dribble Base Visual Design', views: '2.5k', icon: 'tabler-brand-dribbble', color: 'warning' },
|
||||
{ title: 'Your First Singing Lesson', views: '948', icon: 'tabler-microphone-2', color: 'error' }
|
||||
]
|
||||
|
||||
const TopCourses = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Top Courses' action={<OptionMenu options={['Last 28 Days', 'Last Month', 'Last Year']} />} />
|
||||
<CardContent className='flex flex-col gap-6'>
|
||||
{data.map((item, i) => (
|
||||
<div key={i} className='flex items-center gap-4'>
|
||||
<CustomAvatar variant='rounded' skin='light' color={item.color}>
|
||||
<i className={item.icon} />
|
||||
</CustomAvatar>
|
||||
<div className='flex justify-between items-center gap-4 is-full flex-wrap'>
|
||||
<Typography className='font-medium flex-1' color='text.primary'>
|
||||
{item.title}
|
||||
</Typography>
|
||||
<Chip label={`${item.views} Views`} variant='tonal' size='small' color='secondary' />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopCourses
|
||||
@@ -1,61 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Button from '@mui/material/Button'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Components Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
|
||||
type DataType = {
|
||||
icon: string
|
||||
title: string
|
||||
value: string
|
||||
}
|
||||
|
||||
// Vars
|
||||
const data: DataType[] = [
|
||||
{ icon: 'tabler-calendar', title: '17 Nov 23', value: 'Date' },
|
||||
{ icon: 'tabler-clock', title: '32 Minutes', value: 'Duration' }
|
||||
]
|
||||
|
||||
const UpcomingWebinar = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className='flex flex-col gap-4'>
|
||||
<div className='flex justify-center pli-2.5 pbs-4 rounded bg-primaryLight'>
|
||||
<img src='/images/illustrations/characters/4.png' className='bs-[146px]' />
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant='h5' className='mbe-2'>
|
||||
Upcoming Webinar
|
||||
</Typography>
|
||||
<Typography variant='body2'>
|
||||
Next Generation Frontend Architecture Using Layout Engine And React Native Web.
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex flex-wrap justify-between gap-4'>
|
||||
{data.map((item, i) => (
|
||||
<div key={i} className='flex items-center gap-3'>
|
||||
<CustomAvatar variant='rounded' skin='light' color='primary'>
|
||||
<i className={classnames('text-[28px]', item.icon)} />
|
||||
</CustomAvatar>
|
||||
<div>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
{item.title}
|
||||
</Typography>
|
||||
<Typography variant='body2'>{item.value}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button variant='contained'>Join the event</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpcomingWebinar
|
||||
@@ -1,218 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
// MUI Imports
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||
import { lighten, darken, useTheme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import type { ApexOptions } from 'apexcharts'
|
||||
|
||||
// Type Imports
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
|
||||
// Styled Component Imports
|
||||
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
|
||||
|
||||
type DataType = {
|
||||
title: string
|
||||
value: string
|
||||
color: ThemeColor
|
||||
icon: ReactNode
|
||||
}
|
||||
|
||||
// Vars
|
||||
const data: DataType[] = [
|
||||
{
|
||||
title: 'Hours Spent',
|
||||
value: '34h',
|
||||
color: 'primary',
|
||||
icon: (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='38' height='38' viewBox='0 0 38 38' fill='none'>
|
||||
<path
|
||||
opacity='0.2'
|
||||
d='M5.9375 26.125V10.6875C5.9375 10.0576 6.18772 9.45352 6.63312 9.00812C7.07852 8.56272 7.68261 8.3125 8.3125 8.3125H29.6875C30.3174 8.3125 30.9215 8.56272 31.3669 9.00812C31.8123 9.45352 32.0625 10.0576 32.0625 10.6875V26.125H5.9375Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
d='M5.9375 26.125V10.6875C5.9375 10.0576 6.18772 9.45352 6.63312 9.00812C7.07852 8.56272 7.68261 8.3125 8.3125 8.3125H29.6875C30.3174 8.3125 30.9215 8.56272 31.3669 9.00812C31.8123 9.45352 32.0625 10.0576 32.0625 10.6875V26.125M21.375 13.0625H16.625M3.5625 26.125H34.4375V28.5C34.4375 29.1299 34.1873 29.734 33.7419 30.1794C33.2965 30.6248 32.6924 30.875 32.0625 30.875H5.9375C5.30761 30.875 4.70352 30.6248 4.25812 30.1794C3.81272 29.734 3.5625 29.1299 3.5625 28.5V26.125Z'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Test Results',
|
||||
value: '82%',
|
||||
color: 'info',
|
||||
icon: (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='38' height='38' viewBox='0 0 38 38' fill='none'>
|
||||
<path
|
||||
opacity='0.2'
|
||||
d='M11.682 24.7885C10.2683 23.6892 9.1233 22.2826 8.33376 20.6753C7.54423 19.0679 7.13087 17.3019 7.125 15.5111C7.09532 9.06896 12.2758 3.71037 18.718 3.56193C21.2112 3.50283 23.6598 4.2302 25.7164 5.6409C27.7731 7.05159 29.3334 9.07399 30.176 11.4213C31.0187 13.7686 31.1009 16.3216 30.4111 18.7182C29.7213 21.1149 28.2944 23.2335 26.3328 24.7736C25.8995 25.1086 25.5485 25.5382 25.3067 26.0296C25.0648 26.521 24.9386 27.0611 24.9375 27.6088V28.4994C24.9375 28.8144 24.8124 29.1164 24.5897 29.3391C24.367 29.5618 24.0649 29.6869 23.75 29.6869H14.25C13.9351 29.6869 13.633 29.5618 13.4103 29.3391C13.1876 29.1164 13.0625 28.8144 13.0625 28.4994V27.6088C13.0588 27.0652 12.9328 26.5295 12.6938 26.0413C12.4548 25.553 12.109 25.1249 11.682 24.7885Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M25.1507 6.46554C23.2672 5.17364 21.0249 4.50752 18.7416 4.56165L18.7409 4.56167C18.4981 4.56726 18.2571 4.58096 18.0184 4.6025L18.6948 2.5622C21.3978 2.49826 24.0523 3.28688 26.282 4.81625C28.5118 6.34574 30.2035 8.53844 31.1171 11.0834C32.0307 13.6283 32.1199 16.3963 31.372 18.9948C30.6241 21.5933 29.077 23.8903 26.9503 25.5602L26.9443 25.5649L26.9443 25.5648C26.6316 25.8065 26.3783 26.1165 26.2038 26.4711C26.0293 26.8257 25.9382 27.2155 25.9374 27.6107V28.4994C25.9374 29.0796 25.7069 29.636 25.2967 30.0462C24.8865 30.4565 24.3301 30.6869 23.7499 30.6869H14.2499C13.6697 30.6869 13.1133 30.4565 12.7031 30.0462C12.2929 29.636 12.0624 29.0796 12.0624 28.4994V27.6125C12.0592 27.2201 11.968 26.8334 11.7955 26.4809C11.6229 26.1283 11.3734 25.819 11.0654 25.5758L11.7412 23.5373C11.9205 23.6971 12.1055 23.8511 12.2958 23.9991L11.6819 24.7885L12.3008 24.003C12.8456 24.4322 13.2869 24.9786 13.5919 25.6016C13.8968 26.2247 14.0576 26.9083 14.0624 27.602L14.0624 27.6088L14.0624 28.4994C14.0624 28.5492 14.0822 28.5969 14.1173 28.632C14.1525 28.6672 14.2002 28.6869 14.2499 28.6869H23.7499C23.7996 28.6869 23.8473 28.6672 23.8825 28.632C23.9176 28.5969 23.9374 28.5492 23.9374 28.4994V27.6088L23.9374 27.6069C23.9388 26.9067 24.1002 26.2162 24.4093 25.588C24.7179 24.961 25.1655 24.4128 25.7179 23.985C27.5129 22.5747 28.8186 20.6353 29.45 18.4416C30.0817 16.2468 30.0064 13.9088 29.2347 11.7592C28.463 9.60954 27.0341 7.75744 25.1507 6.46554ZM11.7411 23.5373L11.7412 23.5373L18.0184 4.6025L18.0178 4.60255L18.6942 2.56221C11.7041 2.72363 6.09308 8.5318 6.12491 15.5151C6.13137 17.4574 6.57975 19.3728 7.43609 21.1162C8.29203 22.8587 9.53309 24.3837 11.0654 25.5758L11.7411 23.5373ZM11.7411 23.5373C10.7006 22.6103 9.84758 21.4892 9.23122 20.2344C8.50859 18.7632 8.13026 17.1469 8.12489 15.5079L8.12489 15.5065C8.09882 9.84932 12.4635 5.10401 18.0178 4.60255L11.7411 23.5373ZM12.0625 34.437C12.0625 33.8847 12.5102 33.437 13.0625 33.437H24.9375C25.4898 33.437 25.9375 33.8847 25.9375 34.437C25.9375 34.9892 25.4898 35.437 24.9375 35.437H13.0625C12.5102 35.437 12.0625 34.9892 12.0625 34.437ZM20.3695 7.44477C19.825 7.35247 19.3087 7.71906 19.2164 8.26357C19.1241 8.80809 19.4907 9.32434 20.0352 9.41664C21.2825 9.62807 22.4333 10.2214 23.329 11.1148C24.2247 12.0082 24.821 13.1576 25.0356 14.4043C25.1293 14.9485 25.6465 15.3138 26.1907 15.2201C26.735 15.1264 27.1003 14.6092 27.0066 14.065C26.7217 12.4102 25.9303 10.8846 24.7414 9.69879C23.5526 8.51298 22.025 7.72541 20.3695 7.44477Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Course Completed',
|
||||
value: '14',
|
||||
color: 'warning',
|
||||
icon: (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='38' height='38' viewBox='0 0 38 38' fill='none'>
|
||||
<path
|
||||
opacity='0.2'
|
||||
d='M8.08984 29.9102C6.72422 28.5445 7.62969 25.6797 6.93203 24.0023C6.23438 22.325 3.5625 20.8555 3.5625 19C3.5625 17.1445 6.20469 15.7344 6.93203 13.9977C7.65938 12.2609 6.72422 9.45547 8.08984 8.08984C9.45547 6.72422 12.3203 7.62969 13.9977 6.93203C15.675 6.23438 17.1445 3.5625 19 3.5625C20.8555 3.5625 22.2656 6.20469 24.0023 6.93203C25.7391 7.65938 28.5445 6.72422 29.9102 8.08984C31.2758 9.45547 30.3703 12.3203 31.068 13.9977C31.7656 15.675 34.4375 17.1445 34.4375 19C34.4375 20.8555 31.7953 22.2656 31.068 24.0023C30.3406 25.7391 31.2758 28.5445 29.9102 29.9102C28.5445 31.2758 25.6797 30.3703 24.0023 31.068C22.325 31.7656 20.8555 34.4375 19 34.4375C17.1445 34.4375 15.7344 31.7953 13.9977 31.068C12.2609 30.3406 9.45547 31.2758 8.08984 29.9102Z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<path
|
||||
d='M25.5312 15.4375L16.818 23.75L12.4687 19.5937M8.08984 29.9102C6.72422 28.5445 7.62969 25.6797 6.93203 24.0023C6.23437 22.325 3.5625 20.8555 3.5625 19C3.5625 17.1445 6.20469 15.7344 6.93203 13.9977C7.65937 12.2609 6.72422 9.45547 8.08984 8.08984C9.45547 6.72422 12.3203 7.62969 13.9977 6.93203C15.675 6.23437 17.1445 3.5625 19 3.5625C20.8555 3.5625 22.2656 6.20469 24.0023 6.93203C25.7391 7.65937 28.5445 6.72422 29.9102 8.08984C31.2758 9.45547 30.3703 12.3203 31.068 13.9977C31.7656 15.675 34.4375 17.1445 34.4375 19C34.4375 20.8555 31.7953 22.2656 31.068 24.0023C30.3406 25.7391 31.2758 28.5445 29.9102 29.9102C28.5445 31.2758 25.6797 30.3703 24.0023 31.068C22.325 31.7656 20.8555 34.4375 19 34.4375C17.1445 34.4375 15.7344 31.7953 13.9977 31.068C12.2609 30.3406 9.45547 31.2758 8.08984 29.9102Z'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
const WelcomeCard = () => {
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
const belowMdScreen = useMediaQuery(theme.breakpoints.down('md'))
|
||||
|
||||
// Vars
|
||||
const options: ApexOptions = {
|
||||
chart: {
|
||||
sparkline: { enabled: true }
|
||||
},
|
||||
grid: {
|
||||
padding: {
|
||||
left: 20,
|
||||
right: 20
|
||||
}
|
||||
},
|
||||
colors: [
|
||||
darken(theme.palette.success.main, 0.15),
|
||||
darken(theme.palette.success.main, 0.1),
|
||||
'var(--mui-palette-success-main)',
|
||||
lighten(theme.palette.success.main, 0.2),
|
||||
lighten(theme.palette.success.main, 0.4),
|
||||
lighten(theme.palette.success.main, 0.6)
|
||||
],
|
||||
stroke: { width: 0 },
|
||||
legend: { show: false },
|
||||
tooltip: { theme: 'false' },
|
||||
dataLabels: { enabled: false },
|
||||
labels: ['36h', '56h', '16h', '32h', '56h', '16h'],
|
||||
states: {
|
||||
hover: {
|
||||
filter: { type: 'none' }
|
||||
},
|
||||
active: {
|
||||
filter: { type: 'none' }
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
customScale: 0.9,
|
||||
donut: {
|
||||
size: '70%',
|
||||
labels: {
|
||||
show: true,
|
||||
name: {
|
||||
offsetY: 20,
|
||||
fontSize: '0.875rem'
|
||||
},
|
||||
value: {
|
||||
offsetY: -15,
|
||||
fontWeight: 500,
|
||||
fontSize: '1.125rem',
|
||||
formatter: value => `${value}%`,
|
||||
color: 'var(--mui-palette-text-primary)'
|
||||
},
|
||||
total: {
|
||||
show: true,
|
||||
fontSize: '0.8125rem',
|
||||
label: 'Total',
|
||||
color: 'var(--mui-palette-text-disabled)',
|
||||
formatter: () => '231h'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex max-md:flex-col md:items-center gap-6 plb-6'>
|
||||
<div className='md:is-8/12'>
|
||||
<div className='flex items-baseline gap-1 mbe-2'>
|
||||
<Typography variant='h5'>Welcome back,</Typography>
|
||||
<Typography variant='h4'>Felecia 👋🏻</Typography>
|
||||
</div>
|
||||
<div className='mbe-4'>
|
||||
<Typography>Your progress this week is Awesome. let's keep it up</Typography>
|
||||
<Typography>and get a lot of points reward!</Typography>
|
||||
</div>
|
||||
<div className='flex flex-wrap max-md:flex-col justify-between gap-6'>
|
||||
{data.map((item, i) => (
|
||||
<div key={i} className='flex gap-4'>
|
||||
<CustomAvatar variant='rounded' skin='light' size={54} color={item.color}>
|
||||
{item.icon}
|
||||
</CustomAvatar>
|
||||
<div>
|
||||
<Typography className='font-medium'>{item.title}</Typography>
|
||||
<Typography variant='h4' color={`${item.color}.main`}>
|
||||
{item.value}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Divider orientation={belowMdScreen ? 'horizontal' : 'vertical'} flexItem />
|
||||
<div className='flex justify-between md:is-4/12'>
|
||||
<div className='flex flex-col justify-between gap-6'>
|
||||
<div>
|
||||
<Typography variant='h5' className='mbe-1'>
|
||||
Time spendings
|
||||
</Typography>
|
||||
<Typography>Weekly report</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant='h4' className='mbe-2'>
|
||||
231<span className='text-textSecondary'>h</span> 14<span className='text-textSecondary'>m</span>
|
||||
</Typography>
|
||||
<Chip label='+18.4%' variant='tonal' size='small' color='success' />
|
||||
</div>
|
||||
</div>
|
||||
<AppReactApexCharts type='donut' height={189} width={150} options={options} series={[23, 35, 10, 20, 35, 23]} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WelcomeCard
|
||||
@@ -1,85 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Button from '@mui/material/Button'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Types Imports
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
type DataType = {
|
||||
title: string
|
||||
description: string
|
||||
type: string
|
||||
image: string
|
||||
color: ThemeColor
|
||||
imageColorClass?: string
|
||||
bgColorClass?: string
|
||||
}
|
||||
|
||||
// Vars
|
||||
const data: DataType[] = [
|
||||
{
|
||||
title: 'Earn a Certificate',
|
||||
description: 'Get the right professional certificate program for you.',
|
||||
type: 'Programs',
|
||||
image: '/images/illustrations/characters/8.png',
|
||||
color: 'primary',
|
||||
imageColorClass: 'bg-primaryLight',
|
||||
bgColorClass: 'bg-primaryLighter'
|
||||
},
|
||||
{
|
||||
title: 'Best Rated Courses',
|
||||
description: 'Enroll now in the most popular and best rated courses.',
|
||||
type: 'Courses',
|
||||
image: '/images/illustrations/characters/9.png',
|
||||
color: 'error',
|
||||
imageColorClass: 'bg-errorLight',
|
||||
bgColorClass: 'bg-errorLighter'
|
||||
}
|
||||
]
|
||||
|
||||
const ColoredCards = () => {
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
{data.map((item, index) => (
|
||||
<Grid size={{ xs: 12, md: 6 }} key={index}>
|
||||
<div
|
||||
className={classnames(
|
||||
'flex max-sm:flex-col items-center sm:items-start justify-between gap-6 rounded p-6',
|
||||
item.bgColorClass
|
||||
)}
|
||||
>
|
||||
<div className='flex flex-col items-center sm:items-start max-sm:text-center'>
|
||||
<Typography variant='h5' color={`${item.color}.main`} className='mbe-2'>
|
||||
{item.title}
|
||||
</Typography>
|
||||
<Typography className='mbe-4'>{item.description}</Typography>
|
||||
<Button variant='contained' size='small' color={item.color}>{`View ${item.type}`}</Button>
|
||||
</div>
|
||||
<div
|
||||
className={classnames(
|
||||
'flex justify-center rounded min-is-[180px] max-sm:-order-1 pbs-[7px]',
|
||||
item.imageColorClass
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
className={classnames('bs-[120px]', { 'scale-x-[-1]': theme.direction === 'rtl' })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default ColoredCards
|
||||
@@ -1,231 +0,0 @@
|
||||
// React Imports
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Button from '@mui/material/Button'
|
||||
import FormControl from '@mui/material/FormControl'
|
||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||
import LinearProgress from '@mui/material/LinearProgress'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Pagination from '@mui/material/Pagination'
|
||||
import Select from '@mui/material/Select'
|
||||
import Switch from '@mui/material/Switch'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Type Imports
|
||||
import type { Course } from '@/types/apps/academyTypes'
|
||||
import type { Locale } from '@configs/i18n'
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Component Imports
|
||||
import DirectionalIcon from '@components/DirectionalIcon'
|
||||
|
||||
// Util Imports
|
||||
import { getLocalizedUrl } from '@/utils/i18n'
|
||||
|
||||
type ChipColorType = {
|
||||
color: ThemeColor
|
||||
}
|
||||
|
||||
type Props = {
|
||||
courseData?: Course[]
|
||||
searchValue: string
|
||||
}
|
||||
|
||||
const chipColor: { [key: string]: ChipColorType } = {
|
||||
Web: { color: 'primary' },
|
||||
Art: { color: 'success' },
|
||||
'UI/UX': { color: 'error' },
|
||||
Psychology: { color: 'warning' },
|
||||
Design: { color: 'info' }
|
||||
}
|
||||
|
||||
const Courses = (props: Props) => {
|
||||
// Props
|
||||
const { courseData, searchValue } = props
|
||||
|
||||
// States
|
||||
const [course, setCourse] = useState<Course['tags']>('All')
|
||||
const [hideCompleted, setHideCompleted] = useState(true)
|
||||
const [data, setData] = useState<Course[]>([])
|
||||
const [activePage, setActivePage] = useState(0)
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
useEffect(() => {
|
||||
let newData =
|
||||
courseData?.filter(courseItem => {
|
||||
if (course === 'All') return !hideCompleted || courseItem.completedTasks !== courseItem.totalTasks
|
||||
|
||||
return courseItem.tags === course && (!hideCompleted || courseItem.completedTasks !== courseItem.totalTasks)
|
||||
}) ?? []
|
||||
|
||||
if (searchValue) {
|
||||
newData = newData.filter(category => category.courseTitle.toLowerCase().includes(searchValue.toLowerCase()))
|
||||
}
|
||||
|
||||
if (activePage > Math.ceil(newData.length / 6)) setActivePage(0)
|
||||
|
||||
setData(newData)
|
||||
}, [searchValue, activePage, course, hideCompleted, courseData])
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setHideCompleted(e.target.checked)
|
||||
setActivePage(0)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className='flex flex-col gap-6'>
|
||||
<div className='flex flex-wrap items-center justify-between gap-4'>
|
||||
<div>
|
||||
<Typography variant='h5'>My Courses</Typography>
|
||||
<Typography>Total 6 course you have purchased</Typography>
|
||||
</div>
|
||||
<div className='flex flex-wrap items-center gap-y-4 gap-x-6'>
|
||||
<FormControl fullWidth size='small' className='is-[250px] flex-auto'>
|
||||
<Select
|
||||
fullWidth
|
||||
id='select-course'
|
||||
value={course}
|
||||
onChange={e => {
|
||||
setCourse(e.target.value)
|
||||
setActivePage(0)
|
||||
}}
|
||||
labelId='course-select'
|
||||
>
|
||||
<MenuItem value='All'>All Courses</MenuItem>
|
||||
<MenuItem value='Web'>Web</MenuItem>
|
||||
<MenuItem value='Art'>Art</MenuItem>
|
||||
<MenuItem value='UI/UX'>UI/UX</MenuItem>
|
||||
<MenuItem value='Psychology'>Psychology</MenuItem>
|
||||
<MenuItem value='Design'>Design</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControlLabel
|
||||
control={<Switch onChange={handleChange} checked={hideCompleted} />}
|
||||
label='Hide completed'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{data.length > 0 ? (
|
||||
<Grid container spacing={6}>
|
||||
{data.slice(activePage * 6, activePage * 6 + 6).map((item, index) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
|
||||
<div className='border rounded bs-full'>
|
||||
<div className='pli-2 pbs-2'>
|
||||
<Link href={getLocalizedUrl('/apps/academy/course-details', locale as Locale)} className='flex'>
|
||||
<img src={item.tutorImg} alt={item.courseTitle} className='is-full' />
|
||||
</Link>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4 p-5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Chip label={item.tags} variant='tonal' size='small' color={chipColor[item.tags].color} />
|
||||
<div className='flex items-start'>
|
||||
<Typography className='font-medium mie-1'>{item.rating}</Typography>
|
||||
<i className='tabler-star-filled text-warning mie-2' />
|
||||
<Typography>{`(${item.ratingCount})`}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography
|
||||
variant='h5'
|
||||
component={Link}
|
||||
href={getLocalizedUrl('/apps/academy/course-details', locale as Locale)}
|
||||
className='hover:text-primary'
|
||||
>
|
||||
{item.courseTitle}
|
||||
</Typography>
|
||||
<Typography>{item.desc}</Typography>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
{item.completedTasks === item.totalTasks ? (
|
||||
<div className='flex items-center gap-1'>
|
||||
<i className='tabler-check text-xl text-success' />
|
||||
<Typography color='success.main'>Completed</Typography>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-1'>
|
||||
<i className='tabler-clock text-xl' />
|
||||
<Typography>{`${item.time}`}</Typography>
|
||||
</div>
|
||||
)}
|
||||
<LinearProgress
|
||||
color='primary'
|
||||
value={Math.floor((item.completedTasks / item.totalTasks) * 100)}
|
||||
variant='determinate'
|
||||
className='is-full bs-2'
|
||||
/>
|
||||
</div>
|
||||
{item.completedTasks === item.totalTasks ? (
|
||||
<Button
|
||||
variant='tonal'
|
||||
startIcon={<i className='tabler-rotate-clockwise-2' />}
|
||||
component={Link}
|
||||
href={getLocalizedUrl('/apps/academy/course-details', locale as Locale)}
|
||||
>
|
||||
Start Over
|
||||
</Button>
|
||||
) : (
|
||||
<div className='flex flex-wrap gap-4'>
|
||||
<Button
|
||||
fullWidth
|
||||
variant='tonal'
|
||||
color='secondary'
|
||||
startIcon={<i className='tabler-rotate-clockwise-2' />}
|
||||
component={Link}
|
||||
href={getLocalizedUrl('/apps/academy/course-details', locale as Locale)}
|
||||
className='is-auto flex-auto'
|
||||
>
|
||||
Start Over
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
variant='tonal'
|
||||
endIcon={
|
||||
<DirectionalIcon ltrIconClass='tabler-chevron-right' rtlIconClass='tabler-chevron-left' />
|
||||
}
|
||||
component={Link}
|
||||
href={getLocalizedUrl('/apps/academy/course-details', locale as Locale)}
|
||||
className='is-auto flex-auto'
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography className='text-center'>No courses found</Typography>
|
||||
)}
|
||||
<div className='flex justify-center'>
|
||||
<Pagination
|
||||
count={Math.ceil(data.length / 6)}
|
||||
page={activePage + 1}
|
||||
showFirstButton
|
||||
showLastButton
|
||||
shape='rounded'
|
||||
variant='tonal'
|
||||
color='primary'
|
||||
onChange={(e, page) => setActivePage(page - 1)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default Courses
|
||||
@@ -1,94 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Button from '@mui/material/Button'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Third-party Imports
|
||||
import ReactPlayer from '@/libs/ReactPlayer'
|
||||
|
||||
// Components Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import CustomIconButton from '@core/components/mui/IconButton'
|
||||
|
||||
const FreeCourses = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<div className='flex flex-col items-center justify-center gap-y-4 bs-full text-center'>
|
||||
<CustomAvatar variant='rounded' skin='light' color='primary' size={52}>
|
||||
<i className='tabler-gift text-4xl' />
|
||||
</CustomAvatar>
|
||||
<Typography variant='h4'>Today's Free Courses</Typography>
|
||||
<Typography>
|
||||
We offers 284 Free Online courses from top tutors and companies to help you start or advance your career
|
||||
skills. Learn online for free and fast today!
|
||||
</Typography>
|
||||
<Button variant='contained'>Get Premium Courses</Button>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<div className='border rounded bs-full'>
|
||||
<div className='mli-2 mbs-2 overflow-hidden rounded'>
|
||||
<ReactPlayer
|
||||
playing
|
||||
controls
|
||||
url='https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4'
|
||||
height={200}
|
||||
className='bg-black !is-full'
|
||||
light={
|
||||
<img src='/images/apps/academy/7.png' alt='Thumbnail' className='is-full bs-full object-cover' />
|
||||
}
|
||||
playIcon={
|
||||
<CustomIconButton variant='contained' color='error' className='absolute rounded-full'>
|
||||
<i className='tabler-player-play' />
|
||||
</CustomIconButton>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2 p-6'>
|
||||
<Typography variant='h5'>Your First Singing Lesson</Typography>
|
||||
<Typography>
|
||||
In the same way as any other artistic domain, singing lends itself perfectly to self-teaching.
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<div className='border rounded bs-full'>
|
||||
<div className='mli-2 mbs-2 overflow-hidden rounded'>
|
||||
<ReactPlayer
|
||||
playing
|
||||
controls
|
||||
url='https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4'
|
||||
height={200}
|
||||
className='bg-black !is-full'
|
||||
light={
|
||||
<img src='/images/apps/academy/8.png' alt='Thumbnail' className='is-full bs-full object-cover' />
|
||||
}
|
||||
playIcon={
|
||||
<CustomIconButton variant='contained' color='error' className='absolute rounded-full'>
|
||||
<i className='tabler-player-play' />
|
||||
</CustomIconButton>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2 p-6'>
|
||||
<Typography variant='h5'>Guitar for Beginners</Typography>
|
||||
<Typography>
|
||||
The Fender Acoustic Guitar is the best choice for both beginners and professionals offering a great
|
||||
sound.
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default FreeCourses
|
||||
@@ -1,70 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { Mode } from '@core/types'
|
||||
|
||||
// Component Imports
|
||||
import CustomIconButton from '@core/components/mui/IconButton'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Hook Imports
|
||||
import { useImageVariant } from '@core/hooks/useImageVariant'
|
||||
|
||||
type Props = {
|
||||
mode: Mode
|
||||
searchValue: string
|
||||
setSearchValue: (value: string) => void
|
||||
}
|
||||
|
||||
const MyCourseHeader = (props: Props) => {
|
||||
// Props
|
||||
const { mode, searchValue, setSearchValue } = props
|
||||
|
||||
// Vars
|
||||
const lightIllustration = '/images/apps/academy/hand-with-bulb-light.png'
|
||||
const darkIllustration = '/images/apps/academy/hand-with-bulb-dark.png'
|
||||
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
const leftIllustration = useImageVariant(mode, lightIllustration, darkIllustration)
|
||||
|
||||
return (
|
||||
<Card className='relative flex justify-center'>
|
||||
<img src={leftIllustration} className='max-md:hidden absolute max-is-[100px] top-12 start-12' />
|
||||
<div className='flex flex-col items-center gap-4 max-md:pli-5 plb-12 md:is-1/2'>
|
||||
<Typography variant='h4' className='text-center md:is-3/4'>
|
||||
Education, talents, and career opportunities. <span className='text-primary'>All in one place.</span>
|
||||
</Typography>
|
||||
<Typography className='text-center'>
|
||||
Grow your skill with the most reliable online courses and certifications in marketing, information technology,
|
||||
programming, and data science.
|
||||
</Typography>
|
||||
<div className='flex items-center gap-4 max-sm:is-full'>
|
||||
<CustomTextField
|
||||
placeholder='Find your course'
|
||||
value={searchValue}
|
||||
onChange={e => setSearchValue(e.target.value)}
|
||||
className='sm:is-[350px] max-sm:flex-1'
|
||||
/>
|
||||
<CustomIconButton variant='contained' color='primary'>
|
||||
<i className='tabler-search' />
|
||||
</CustomIconButton>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src='/images/apps/academy/9.png'
|
||||
className={classnames('max-md:hidden absolute max-bs-[180px] bottom-0 end-0', {
|
||||
'scale-x-[-1]': theme.direction === 'rtl'
|
||||
})}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyCourseHeader
|
||||
@@ -1,46 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Type Imports
|
||||
import type { Mode } from '@core/types'
|
||||
import type { Course } from '@/types/apps/academyTypes'
|
||||
|
||||
// Component Imports
|
||||
import MyCourseHeader from './MyCourseHeader'
|
||||
import Courses from './Courses'
|
||||
import ColoredCards from './ColoredCards'
|
||||
import FreeCourses from './FreeCourses'
|
||||
|
||||
type Props = {
|
||||
courseData?: Course[]
|
||||
mode: Mode
|
||||
}
|
||||
|
||||
const AcademyMyCourse = ({ courseData, mode }: Props) => {
|
||||
// States
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<MyCourseHeader mode={mode} searchValue={searchValue} setSearchValue={setSearchValue} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Courses courseData={courseData} searchValue={searchValue} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<ColoredCards />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<FreeCourses />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default AcademyMyCourse
|
||||
@@ -1,359 +0,0 @@
|
||||
// React Imports
|
||||
import { useState, useEffect, forwardRef, useCallback } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Box from '@mui/material/Box'
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import Switch from '@mui/material/Switch'
|
||||
import Button from '@mui/material/Button'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||
import FormControl from '@mui/material/FormControl'
|
||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||
import type { SelectChangeEvent } from '@mui/material/Select'
|
||||
import type { Theme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import { useForm, Controller } from 'react-hook-form'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// Type Imports
|
||||
import type { AddEventSidebarType, AddEventType } from '@/types/apps/calendarTypes'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Styled Component Imports
|
||||
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
|
||||
|
||||
// Slice Imports
|
||||
import { addEvent, deleteEvent, updateEvent, selectedEvent, filterEvents } from '@/redux-store/slices/calendar'
|
||||
|
||||
interface PickerProps {
|
||||
label?: string
|
||||
error?: boolean
|
||||
registername?: string
|
||||
}
|
||||
|
||||
interface DefaultStateType {
|
||||
url: string
|
||||
title: string
|
||||
allDay: boolean
|
||||
calendar: string
|
||||
description: string
|
||||
endDate: Date
|
||||
startDate: Date
|
||||
guests: string[] | undefined
|
||||
}
|
||||
|
||||
// Vars
|
||||
const capitalize = (string: string) => string && string[0].toUpperCase() + string.slice(1)
|
||||
|
||||
// Vars
|
||||
const defaultState: DefaultStateType = {
|
||||
url: '',
|
||||
title: '',
|
||||
guests: [],
|
||||
allDay: true,
|
||||
description: '',
|
||||
endDate: new Date(),
|
||||
calendar: 'Business',
|
||||
startDate: new Date()
|
||||
}
|
||||
|
||||
const AddEventSidebar = (props: AddEventSidebarType) => {
|
||||
// Props
|
||||
const { calendarStore, dispatch, addEventSidebarOpen, handleAddEventSidebarToggle } = props
|
||||
|
||||
// States
|
||||
const [values, setValues] = useState<DefaultStateType>(defaultState)
|
||||
|
||||
// Refs
|
||||
const PickersComponent = forwardRef(({ ...props }: PickerProps, ref) => {
|
||||
return (
|
||||
<CustomTextField
|
||||
inputRef={ref}
|
||||
fullWidth
|
||||
{...props}
|
||||
label={props.label || ''}
|
||||
className='is-full'
|
||||
error={props.error}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
// Hooks
|
||||
const isBelowSmScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
|
||||
|
||||
const {
|
||||
control,
|
||||
setValue,
|
||||
clearErrors,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm({ defaultValues: { title: '' } })
|
||||
|
||||
const resetToStoredValues = useCallback(() => {
|
||||
if (calendarStore.selectedEvent !== null) {
|
||||
const event = calendarStore.selectedEvent
|
||||
|
||||
setValue('title', event.title || '')
|
||||
setValues({
|
||||
url: event.url || '',
|
||||
title: event.title || '',
|
||||
allDay: event.allDay,
|
||||
guests: event.extendedProps.guests || [],
|
||||
description: event.extendedProps.description || '',
|
||||
calendar: event.extendedProps.calendar || 'Business',
|
||||
endDate: event.end !== null ? event.end : event.start,
|
||||
startDate: event.start !== null ? event.start : new Date()
|
||||
})
|
||||
}
|
||||
}, [setValue, calendarStore.selectedEvent])
|
||||
|
||||
const resetToEmptyValues = useCallback(() => {
|
||||
setValue('title', '')
|
||||
setValues(defaultState)
|
||||
}, [setValue])
|
||||
|
||||
const handleSidebarClose = () => {
|
||||
setValues(defaultState)
|
||||
clearErrors()
|
||||
dispatch(selectedEvent(null))
|
||||
handleAddEventSidebarToggle()
|
||||
}
|
||||
|
||||
const onSubmit = (data: { title: string }) => {
|
||||
const modifiedEvent: AddEventType = {
|
||||
url: values.url,
|
||||
display: 'block',
|
||||
title: data.title,
|
||||
end: values.endDate,
|
||||
allDay: values.allDay,
|
||||
start: values.startDate,
|
||||
extendedProps: {
|
||||
calendar: capitalize(values.calendar),
|
||||
guests: values.guests && values.guests.length ? values.guests : undefined,
|
||||
description: values.description.length ? values.description : undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
calendarStore.selectedEvent === null ||
|
||||
(calendarStore.selectedEvent !== null && !calendarStore.selectedEvent.title.length)
|
||||
) {
|
||||
dispatch(addEvent(modifiedEvent))
|
||||
} else {
|
||||
dispatch(updateEvent({ ...modifiedEvent, id: calendarStore.selectedEvent.id }))
|
||||
}
|
||||
|
||||
dispatch(filterEvents())
|
||||
|
||||
handleSidebarClose()
|
||||
}
|
||||
|
||||
const handleDeleteButtonClick = () => {
|
||||
if (calendarStore.selectedEvent) {
|
||||
dispatch(deleteEvent(calendarStore.selectedEvent.id))
|
||||
dispatch(filterEvents())
|
||||
}
|
||||
|
||||
// calendarApi.getEventById(calendarStore.selectedEvent.id).remove()
|
||||
handleSidebarClose()
|
||||
}
|
||||
|
||||
const handleStartDate = (date: Date | null) => {
|
||||
if (date && date > values.endDate) {
|
||||
setValues({ ...values, startDate: new Date(date), endDate: new Date(date) })
|
||||
}
|
||||
}
|
||||
|
||||
const RenderSidebarFooter = () => {
|
||||
if (
|
||||
calendarStore.selectedEvent === null ||
|
||||
(calendarStore.selectedEvent && !calendarStore.selectedEvent.title.length)
|
||||
) {
|
||||
return (
|
||||
<div className='flex gap-4'>
|
||||
<Button type='submit' variant='contained'>
|
||||
Add
|
||||
</Button>
|
||||
<Button variant='outlined' color='secondary' onClick={resetToEmptyValues}>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className='flex gap-4'>
|
||||
<Button type='submit' variant='contained'>
|
||||
Update
|
||||
</Button>
|
||||
<Button variant='outlined' color='secondary' onClick={resetToStoredValues}>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const ScrollWrapper = isBelowSmScreen ? 'div' : PerfectScrollbar
|
||||
|
||||
useEffect(() => {
|
||||
if (calendarStore.selectedEvent !== null) {
|
||||
resetToStoredValues()
|
||||
} else {
|
||||
resetToEmptyValues()
|
||||
}
|
||||
}, [addEventSidebarOpen, resetToStoredValues, resetToEmptyValues, calendarStore.selectedEvent])
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
anchor='right'
|
||||
open={addEventSidebarOpen}
|
||||
onClose={handleSidebarClose}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
sx={{ '& .MuiDrawer-paper': { width: ['100%', 400] } }}
|
||||
>
|
||||
<Box className='flex justify-between items-center sidebar-header plb-5 pli-6 border-be'>
|
||||
<Typography variant='h5'>
|
||||
{calendarStore.selectedEvent && calendarStore.selectedEvent.title.length ? 'Update Event' : 'Add Event'}
|
||||
</Typography>
|
||||
{calendarStore.selectedEvent && calendarStore.selectedEvent.title.length ? (
|
||||
<Box className='flex items-center' sx={{ gap: calendarStore.selectedEvent !== null ? 1 : 0 }}>
|
||||
<IconButton size='small' onClick={handleDeleteButtonClick}>
|
||||
<i className='tabler-trash text-2xl text-textPrimary' />
|
||||
</IconButton>
|
||||
<IconButton size='small' onClick={handleSidebarClose}>
|
||||
<i className='tabler-x text-2xl text-textPrimary' />
|
||||
</IconButton>
|
||||
</Box>
|
||||
) : (
|
||||
<IconButton size='small' onClick={handleSidebarClose}>
|
||||
<i className='tabler-x text-2xl text-textPrimary' />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
<ScrollWrapper
|
||||
{...(isBelowSmScreen
|
||||
? { className: 'bs-full overflow-y-auto overflow-x-hidden' }
|
||||
: { options: { wheelPropagation: false, suppressScrollX: true } })}
|
||||
>
|
||||
<Box className='sidebar-body plb-5 pli-6'>
|
||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete='off' className='flex flex-col gap-6'>
|
||||
<Controller
|
||||
name='title'
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Title'
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
{...(errors.title && { error: true, helperText: 'This field is required' })}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<CustomTextField
|
||||
select
|
||||
fullWidth
|
||||
label='Calendar'
|
||||
value={values.calendar}
|
||||
onChange={e => setValues({ ...values, calendar: e.target.value })}
|
||||
>
|
||||
<MenuItem value='Personal'>Personal</MenuItem>
|
||||
<MenuItem value='Business'>Business</MenuItem>
|
||||
<MenuItem value='Family'>Family</MenuItem>
|
||||
<MenuItem value='Holiday'>Holiday</MenuItem>
|
||||
<MenuItem value='ETC'>ETC</MenuItem>
|
||||
</CustomTextField>
|
||||
|
||||
<AppReactDatepicker
|
||||
selectsStart
|
||||
id='event-start-date'
|
||||
endDate={values.endDate}
|
||||
selected={values.startDate}
|
||||
startDate={values.startDate}
|
||||
showTimeSelect={!values.allDay}
|
||||
dateFormat={!values.allDay ? 'yyyy-MM-dd hh:mm' : 'yyyy-MM-dd'}
|
||||
customInput={<PickersComponent label='Start Date' registername='startDate' />}
|
||||
onChange={(date: Date | null) => date !== null && setValues({ ...values, startDate: new Date(date) })}
|
||||
onSelect={handleStartDate}
|
||||
/>
|
||||
<AppReactDatepicker
|
||||
selectsEnd
|
||||
id='event-end-date'
|
||||
endDate={values.endDate}
|
||||
selected={values.endDate}
|
||||
minDate={values.startDate}
|
||||
startDate={values.startDate}
|
||||
showTimeSelect={!values.allDay}
|
||||
dateFormat={!values.allDay ? 'yyyy-MM-dd hh:mm' : 'yyyy-MM-dd'}
|
||||
customInput={<PickersComponent label='End Date' registername='endDate' />}
|
||||
onChange={(date: Date | null) => date !== null && setValues({ ...values, endDate: new Date(date) })}
|
||||
/>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
label='All Day'
|
||||
control={
|
||||
<Switch checked={values.allDay} onChange={e => setValues({ ...values, allDay: e.target.checked })} />
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
type='url'
|
||||
id='event-url'
|
||||
label='Event URL'
|
||||
value={values.url}
|
||||
onChange={e => setValues({ ...values, url: e.target.value })}
|
||||
/>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
select
|
||||
label='Guests'
|
||||
value={values.guests}
|
||||
id='event-guests-select'
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
onChange={(e: SelectChangeEvent<(typeof values)['guests']>) => {
|
||||
setValues({
|
||||
...values,
|
||||
guests: typeof e.target.value === 'string' ? e.target.value.split(',') : e.target.value
|
||||
})
|
||||
}}
|
||||
slotProps={{
|
||||
select: {
|
||||
multiple: true
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value='bruce'>Bruce</MenuItem>
|
||||
<MenuItem value='clark'>Clark</MenuItem>
|
||||
<MenuItem value='diana'>Diana</MenuItem>
|
||||
<MenuItem value='john'>John</MenuItem>
|
||||
<MenuItem value='barry'>Barry</MenuItem>
|
||||
</CustomTextField>
|
||||
<CustomTextField
|
||||
rows={4}
|
||||
multiline
|
||||
fullWidth
|
||||
label='Description'
|
||||
id='event-description'
|
||||
value={values.description}
|
||||
onChange={e => setValues({ ...values, description: e.target.value })}
|
||||
/>
|
||||
<div className='flex items-center'>
|
||||
<RenderSidebarFooter />
|
||||
</div>
|
||||
</form>
|
||||
</Box>
|
||||
</ScrollWrapper>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddEventSidebar
|
||||
@@ -1,193 +0,0 @@
|
||||
// React Imports
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Third-party imports
|
||||
import type { Dispatch } from '@reduxjs/toolkit'
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css'
|
||||
|
||||
import FullCalendar from '@fullcalendar/react'
|
||||
import listPlugin from '@fullcalendar/list'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import type { CalendarOptions } from '@fullcalendar/core'
|
||||
|
||||
// Type Imports
|
||||
import type { AddEventType, CalendarColors, CalendarType } from '@/types/apps/calendarTypes'
|
||||
|
||||
// Slice Imports
|
||||
import { filterEvents, selectedEvent, updateEvent } from '@/redux-store/slices/calendar'
|
||||
|
||||
type CalenderProps = {
|
||||
calendarStore: CalendarType
|
||||
calendarApi: any
|
||||
setCalendarApi: (val: any) => void
|
||||
calendarsColor: CalendarColors
|
||||
dispatch: Dispatch
|
||||
handleLeftSidebarToggle: () => void
|
||||
handleAddEventSidebarToggle: () => void
|
||||
}
|
||||
|
||||
const blankEvent: AddEventType = {
|
||||
title: '',
|
||||
start: '',
|
||||
end: '',
|
||||
allDay: false,
|
||||
url: '',
|
||||
extendedProps: {
|
||||
calendar: '',
|
||||
guests: [],
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
const Calendar = (props: CalenderProps) => {
|
||||
// Props
|
||||
const {
|
||||
calendarStore,
|
||||
calendarApi,
|
||||
setCalendarApi,
|
||||
calendarsColor,
|
||||
dispatch,
|
||||
handleAddEventSidebarToggle,
|
||||
handleLeftSidebarToggle
|
||||
} = props
|
||||
|
||||
// Refs
|
||||
const calendarRef = useRef()
|
||||
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
if (calendarApi === null) {
|
||||
// @ts-ignore
|
||||
setCalendarApi(calendarRef.current?.getApi())
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// calendarOptions(Props)
|
||||
const calendarOptions: CalendarOptions = {
|
||||
events: calendarStore.events,
|
||||
plugins: [interactionPlugin, dayGridPlugin, timeGridPlugin, listPlugin],
|
||||
initialView: 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
start: 'sidebarToggle, prev, next, title',
|
||||
end: 'dayGridMonth,timeGridWeek,timeGridDay,listMonth'
|
||||
},
|
||||
views: {
|
||||
week: {
|
||||
titleFormat: { year: 'numeric', month: 'short', day: 'numeric' }
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
Enable dragging and resizing event
|
||||
? Docs: https://fullcalendar.io/docs/editable
|
||||
*/
|
||||
editable: true,
|
||||
|
||||
/*
|
||||
Enable resizing event from start
|
||||
? Docs: https://fullcalendar.io/docs/eventResizableFromStart
|
||||
*/
|
||||
eventResizableFromStart: true,
|
||||
|
||||
/*
|
||||
Automatically scroll the scroll-containers during event drag-and-drop and date selecting
|
||||
? Docs: https://fullcalendar.io/docs/dragScroll
|
||||
*/
|
||||
dragScroll: true,
|
||||
|
||||
/*
|
||||
Max number of events within a given day
|
||||
? Docs: https://fullcalendar.io/docs/dayMaxEvents
|
||||
*/
|
||||
dayMaxEvents: 2,
|
||||
|
||||
/*
|
||||
Determines if day names and week names are clickable
|
||||
? Docs: https://fullcalendar.io/docs/navLinks
|
||||
*/
|
||||
navLinks: true,
|
||||
|
||||
eventClassNames({ event: calendarEvent }: any) {
|
||||
// @ts-ignore
|
||||
const colorName = calendarsColor[calendarEvent._def.extendedProps.calendar]
|
||||
|
||||
return [
|
||||
// Background Color
|
||||
`event-bg-${colorName}`
|
||||
]
|
||||
},
|
||||
|
||||
eventClick({ event: clickedEvent, jsEvent }: any) {
|
||||
jsEvent.preventDefault()
|
||||
|
||||
dispatch(selectedEvent(clickedEvent))
|
||||
handleAddEventSidebarToggle()
|
||||
|
||||
if (clickedEvent.url) {
|
||||
// Open the URL in a new tab
|
||||
window.open(clickedEvent.url, '_blank')
|
||||
}
|
||||
|
||||
//* Only grab required field otherwise it goes in infinity loop
|
||||
//! Always grab all fields rendered by form (even if it get `undefined`)
|
||||
// event.value = grabEventDataFromEventApi(clickedEvent)
|
||||
// isAddNewEventSidebarActive.value = true
|
||||
},
|
||||
|
||||
customButtons: {
|
||||
sidebarToggle: {
|
||||
icon: 'tabler tabler-menu-2',
|
||||
click() {
|
||||
handleLeftSidebarToggle()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
dateClick(info: any) {
|
||||
const ev = { ...blankEvent }
|
||||
|
||||
ev.start = info.date
|
||||
ev.end = info.date
|
||||
ev.allDay = true
|
||||
|
||||
dispatch(selectedEvent(ev))
|
||||
handleAddEventSidebarToggle()
|
||||
},
|
||||
|
||||
/*
|
||||
Handle event drop (Also include dragged event)
|
||||
? Docs: https://fullcalendar.io/docs/eventDrop
|
||||
? We can use `eventDragStop` but it doesn't return updated event so we have to use `eventDrop` which returns updated event
|
||||
*/
|
||||
eventDrop({ event: droppedEvent }: any) {
|
||||
dispatch(updateEvent(droppedEvent))
|
||||
dispatch(filterEvents())
|
||||
},
|
||||
|
||||
/*
|
||||
Handle event resize
|
||||
? Docs: https://fullcalendar.io/docs/eventResize
|
||||
*/
|
||||
eventResize({ event: resizedEvent }: any) {
|
||||
dispatch(updateEvent(resizedEvent))
|
||||
dispatch(filterEvents())
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
ref: calendarRef,
|
||||
|
||||
direction: theme.direction
|
||||
}
|
||||
|
||||
return <FullCalendar {...calendarOptions} />
|
||||
}
|
||||
|
||||
export default Calendar
|
||||
@@ -1,79 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import { useMediaQuery } from '@mui/material'
|
||||
import type { Theme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
// Type Imports
|
||||
import type { CalendarColors, CalendarType } from '@/types/apps/calendarTypes'
|
||||
|
||||
// Component Imports
|
||||
import Calendar from './Calendar'
|
||||
import SidebarLeft from './SidebarLeft'
|
||||
import AddEventSidebar from './AddEventSidebar'
|
||||
|
||||
// CalendarColors Object
|
||||
const calendarsColor: CalendarColors = {
|
||||
Personal: 'error',
|
||||
Business: 'primary',
|
||||
Family: 'warning',
|
||||
Holiday: 'success',
|
||||
ETC: 'info'
|
||||
}
|
||||
|
||||
const AppCalendar = () => {
|
||||
// States
|
||||
const [calendarApi, setCalendarApi] = useState<null | any>(null)
|
||||
const [leftSidebarOpen, setLeftSidebarOpen] = useState<boolean>(false)
|
||||
const [addEventSidebarOpen, setAddEventSidebarOpen] = useState<boolean>(false)
|
||||
|
||||
// Hooks
|
||||
const dispatch = useDispatch()
|
||||
const calendarStore = useSelector((state: { calendarReducer: CalendarType }) => state.calendarReducer)
|
||||
const mdAbove = useMediaQuery((theme: Theme) => theme.breakpoints.up('md'))
|
||||
|
||||
const handleLeftSidebarToggle = () => setLeftSidebarOpen(!leftSidebarOpen)
|
||||
|
||||
const handleAddEventSidebarToggle = () => setAddEventSidebarOpen(!addEventSidebarOpen)
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarLeft
|
||||
mdAbove={mdAbove}
|
||||
dispatch={dispatch}
|
||||
calendarApi={calendarApi}
|
||||
calendarStore={calendarStore}
|
||||
calendarsColor={calendarsColor}
|
||||
leftSidebarOpen={leftSidebarOpen}
|
||||
handleLeftSidebarToggle={handleLeftSidebarToggle}
|
||||
handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
||||
/>
|
||||
<div className='p-6 pbe-0 flex-grow overflow-visible bg-backgroundPaper rounded'>
|
||||
<Calendar
|
||||
dispatch={dispatch}
|
||||
calendarApi={calendarApi}
|
||||
calendarStore={calendarStore}
|
||||
setCalendarApi={setCalendarApi}
|
||||
calendarsColor={calendarsColor}
|
||||
handleLeftSidebarToggle={handleLeftSidebarToggle}
|
||||
handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
||||
/>
|
||||
</div>
|
||||
<AddEventSidebar
|
||||
dispatch={dispatch}
|
||||
calendarApi={calendarApi}
|
||||
calendarStore={calendarStore}
|
||||
addEventSidebarOpen={addEventSidebarOpen}
|
||||
handleAddEventSidebarToggle={handleAddEventSidebarToggle}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppCalendar
|
||||
@@ -1,137 +0,0 @@
|
||||
// MUI Imports
|
||||
import Button from '@mui/material/Button'
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||
|
||||
// Third-party imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Types Imports
|
||||
import type { SidebarLeftProps, CalendarFiltersType } from '@/types/apps/calendarTypes'
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Styled Component Imports
|
||||
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
|
||||
|
||||
// Slice Imports
|
||||
import { filterAllCalendarLabels, filterCalendarLabel, selectedEvent } from '@/redux-store/slices/calendar'
|
||||
|
||||
const SidebarLeft = (props: SidebarLeftProps) => {
|
||||
// Props
|
||||
const {
|
||||
mdAbove,
|
||||
leftSidebarOpen,
|
||||
calendarStore,
|
||||
calendarsColor,
|
||||
calendarApi,
|
||||
dispatch,
|
||||
handleLeftSidebarToggle,
|
||||
handleAddEventSidebarToggle
|
||||
} = props
|
||||
|
||||
// Vars
|
||||
const colorsArr = calendarsColor ? Object.entries(calendarsColor) : []
|
||||
|
||||
const renderFilters = colorsArr.length
|
||||
? colorsArr.map(([key, value]: string[]) => {
|
||||
return (
|
||||
<FormControlLabel
|
||||
className='mbe-1'
|
||||
key={key}
|
||||
label={key}
|
||||
control={
|
||||
<Checkbox
|
||||
color={value as ThemeColor}
|
||||
checked={calendarStore.selectedCalendars.indexOf(key as CalendarFiltersType) > -1}
|
||||
onChange={() => dispatch(filterCalendarLabel(key as CalendarFiltersType))}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null
|
||||
|
||||
const handleSidebarToggleSidebar = () => {
|
||||
dispatch(selectedEvent(null))
|
||||
handleAddEventSidebarToggle()
|
||||
}
|
||||
|
||||
if (renderFilters) {
|
||||
return (
|
||||
<Drawer
|
||||
open={leftSidebarOpen}
|
||||
onClose={handleLeftSidebarToggle}
|
||||
variant={mdAbove ? 'permanent' : 'temporary'}
|
||||
ModalProps={{
|
||||
disablePortal: true,
|
||||
disableAutoFocus: true,
|
||||
disableScrollLock: true,
|
||||
keepMounted: true // Better open performance on mobile.
|
||||
}}
|
||||
className={classnames('block', { static: mdAbove, absolute: !mdAbove })}
|
||||
PaperProps={{
|
||||
className: classnames('items-start is-[280px] shadow-none rounded rounded-se-none rounded-ee-none', {
|
||||
static: mdAbove,
|
||||
absolute: !mdAbove
|
||||
})
|
||||
}}
|
||||
sx={{
|
||||
zIndex: 3,
|
||||
'& .MuiDrawer-paper': {
|
||||
zIndex: mdAbove ? 2 : 'drawer'
|
||||
},
|
||||
'& .MuiBackdrop-root': {
|
||||
borderRadius: 1,
|
||||
position: 'absolute'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='is-full p-6'>
|
||||
<Button
|
||||
fullWidth
|
||||
variant='contained'
|
||||
onClick={handleSidebarToggleSidebar}
|
||||
startIcon={<i className='tabler-plus' />}
|
||||
>
|
||||
Add Event
|
||||
</Button>
|
||||
</div>
|
||||
<Divider className='is-full' />
|
||||
<AppReactDatepicker
|
||||
inline
|
||||
onChange={date => calendarApi.gotoDate(date)}
|
||||
boxProps={{
|
||||
className: 'flex justify-center is-full',
|
||||
sx: { '& .react-datepicker': { boxShadow: 'none !important', border: 'none !important' } }
|
||||
}}
|
||||
/>
|
||||
<Divider className='is-full' />
|
||||
|
||||
<div className='flex flex-col p-6 is-full'>
|
||||
<Typography variant='h5' className='mbe-4'>
|
||||
Event Filters
|
||||
</Typography>
|
||||
<FormControlLabel
|
||||
className='mbe-1'
|
||||
label='View All'
|
||||
control={
|
||||
<Checkbox
|
||||
color='secondary'
|
||||
checked={calendarStore.selectedCalendars.length === colorsArr.length}
|
||||
onChange={e => dispatch(filterAllCalendarLabels(e.target.checked))}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{renderFilters}
|
||||
</div>
|
||||
</Drawer>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default SidebarLeft
|
||||
@@ -1,72 +0,0 @@
|
||||
// React Imports
|
||||
import type { MouseEvent, RefObject } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Badge from '@mui/material/Badge'
|
||||
import Avatar from '@mui/material/Avatar'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
|
||||
// Util Imports
|
||||
import { getInitials } from '@/utils/getInitials'
|
||||
|
||||
const BadgeContentSpan = styled('span', {
|
||||
name: 'MuiBadgeContentSpan'
|
||||
})<{ color: ThemeColor; badgeSize: number }>(({ color, badgeSize }) => ({
|
||||
width: badgeSize,
|
||||
height: badgeSize,
|
||||
borderRadius: '50%',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: `var(--mui-palette-${color}-main)`,
|
||||
boxShadow: '0 0 0 2px var(--mui-palette-background-paper)'
|
||||
}))
|
||||
|
||||
type AvatarWithBadgeProps = {
|
||||
ref?: RefObject<HTMLDivElement>
|
||||
alt?: string
|
||||
src?: string
|
||||
color?: ThemeColor
|
||||
badgeColor?: ThemeColor
|
||||
isChatActive?: boolean
|
||||
onClick?: (e: MouseEvent<HTMLDivElement>) => void
|
||||
className?: string
|
||||
badgeSize?: number
|
||||
}
|
||||
|
||||
const AvatarWithBadge = (props: AvatarWithBadgeProps) => {
|
||||
// Props
|
||||
const { ref, alt, src, color, badgeColor, isChatActive, onClick, className, badgeSize } = props
|
||||
|
||||
return (
|
||||
<Badge
|
||||
ref={ref}
|
||||
overlap='circular'
|
||||
badgeContent={<BadgeContentSpan color={badgeColor as ThemeColor} onClick={onClick} badgeSize={badgeSize || 8} />}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
>
|
||||
{src ? (
|
||||
<Avatar ref={ref} alt={alt} src={src} onClick={onClick} className={classnames('cursor-pointer', className)} />
|
||||
) : (
|
||||
<CustomAvatar
|
||||
ref={ref}
|
||||
color={color}
|
||||
skin={isChatActive ? 'light-static' : 'light'}
|
||||
onClick={onClick}
|
||||
className={classnames('cursor-pointer', className)}
|
||||
>
|
||||
{alt && getInitials(alt)}
|
||||
</CustomAvatar>
|
||||
)}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
export default AvatarWithBadge
|
||||
@@ -1,227 +0,0 @@
|
||||
// React Imports
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { RefObject } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Button from '@mui/material/Button'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
|
||||
// Type Imports
|
||||
import type { AppDispatch } from '@/redux-store'
|
||||
import type { ChatDataType, ContactType } from '@/types/apps/chatTypes'
|
||||
|
||||
// Component Imports
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
import AvatarWithBadge from './AvatarWithBadge'
|
||||
import { statusObj } from './SidebarLeft'
|
||||
import ChatLog from './ChatLog'
|
||||
import SendMsgForm from './SendMsgForm'
|
||||
import UserProfileRight from './UserProfileRight'
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
|
||||
type Props = {
|
||||
chatStore: ChatDataType
|
||||
dispatch: AppDispatch
|
||||
backdropOpen: boolean
|
||||
setBackdropOpen: (open: boolean) => void
|
||||
setSidebarOpen: (open: boolean) => void
|
||||
isBelowMdScreen: boolean
|
||||
isBelowLgScreen: boolean
|
||||
isBelowSmScreen: boolean
|
||||
messageInputRef: RefObject<HTMLDivElement>
|
||||
}
|
||||
|
||||
// Renders the user avatar with badge and user information
|
||||
const UserAvatar = ({
|
||||
activeUser,
|
||||
setUserProfileLeftOpen,
|
||||
setBackdropOpen
|
||||
}: {
|
||||
activeUser: ContactType
|
||||
setUserProfileLeftOpen: (open: boolean) => void
|
||||
setBackdropOpen: (open: boolean) => void
|
||||
}) => (
|
||||
<div
|
||||
className='flex items-center gap-4 cursor-pointer'
|
||||
onClick={() => {
|
||||
setUserProfileLeftOpen(true)
|
||||
setBackdropOpen(true)
|
||||
}}
|
||||
>
|
||||
<AvatarWithBadge
|
||||
alt={activeUser?.fullName}
|
||||
src={activeUser?.avatar}
|
||||
color={activeUser?.avatarColor}
|
||||
badgeColor={statusObj[activeUser?.status || 'offline']}
|
||||
/>
|
||||
<div>
|
||||
<Typography color='text.primary'>{activeUser?.fullName}</Typography>
|
||||
<Typography variant='body2'>{activeUser?.role}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const ChatContent = (props: Props) => {
|
||||
// Props
|
||||
const {
|
||||
chatStore,
|
||||
dispatch,
|
||||
backdropOpen,
|
||||
setBackdropOpen,
|
||||
setSidebarOpen,
|
||||
isBelowMdScreen,
|
||||
isBelowSmScreen,
|
||||
isBelowLgScreen,
|
||||
messageInputRef
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [userProfileRightOpen, setUserProfileRightOpen] = useState(false)
|
||||
|
||||
// Vars
|
||||
const { activeUser } = chatStore
|
||||
|
||||
useEffect(() => {
|
||||
if (!backdropOpen && userProfileRightOpen) {
|
||||
setUserProfileRightOpen(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [backdropOpen])
|
||||
|
||||
return !chatStore.activeUser ? (
|
||||
<CardContent className='flex flex-col flex-auto items-center justify-center bs-full gap-[18px] bg-backgroundChat'>
|
||||
<CustomAvatar variant='circular' size={98} color='primary' skin='light'>
|
||||
<i className='tabler-message-2 text-[50px]' />
|
||||
</CustomAvatar>
|
||||
<Typography className='text-center'>Select a contact to start a conversation.</Typography>
|
||||
{isBelowMdScreen && (
|
||||
<Button
|
||||
variant='contained'
|
||||
className='rounded-full'
|
||||
onClick={() => {
|
||||
setSidebarOpen(true)
|
||||
isBelowSmScreen ? setBackdropOpen(false) : setBackdropOpen(true)
|
||||
}}
|
||||
>
|
||||
Select Contact
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
) : (
|
||||
<>
|
||||
{activeUser && (
|
||||
<div className='flex flex-col flex-grow bs-full bg-backgroundChat'>
|
||||
<div className='flex items-center justify-between border-be plb-[17px] pli-6 bg-backgroundPaper'>
|
||||
{isBelowMdScreen ? (
|
||||
<div className='flex items-center gap-4'>
|
||||
<IconButton
|
||||
color='secondary'
|
||||
onClick={() => {
|
||||
setSidebarOpen(true)
|
||||
setBackdropOpen(true)
|
||||
}}
|
||||
>
|
||||
<i className='tabler-menu-2' />
|
||||
</IconButton>
|
||||
<UserAvatar
|
||||
activeUser={activeUser}
|
||||
setBackdropOpen={setBackdropOpen}
|
||||
setUserProfileLeftOpen={setUserProfileRightOpen}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<UserAvatar
|
||||
activeUser={activeUser}
|
||||
setBackdropOpen={setBackdropOpen}
|
||||
setUserProfileLeftOpen={setUserProfileRightOpen}
|
||||
/>
|
||||
)}
|
||||
{isBelowMdScreen ? (
|
||||
<OptionMenu
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
iconClassName='text-secondary'
|
||||
options={[
|
||||
{
|
||||
text: 'View Contact',
|
||||
menuItemProps: {
|
||||
onClick: () => {
|
||||
setUserProfileRightOpen(true)
|
||||
setBackdropOpen(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
'Mute Notifications',
|
||||
'Block Contact',
|
||||
'Clear Chat',
|
||||
'Block'
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<div className='flex items-center gap-1'>
|
||||
<IconButton color='secondary'>
|
||||
<i className='tabler-phone' />
|
||||
</IconButton>
|
||||
<IconButton color='secondary'>
|
||||
<i className='tabler-video' />
|
||||
</IconButton>
|
||||
<IconButton color='secondary'>
|
||||
<i className='tabler-search' />
|
||||
</IconButton>
|
||||
<OptionMenu
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
iconClassName='text-secondary'
|
||||
options={[
|
||||
{
|
||||
text: 'View Contact',
|
||||
menuItemProps: {
|
||||
onClick: () => {
|
||||
setUserProfileRightOpen(true)
|
||||
setBackdropOpen(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
'Mute Notifications',
|
||||
'Block Contact',
|
||||
'Clear Chat',
|
||||
'Block'
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ChatLog
|
||||
chatStore={chatStore}
|
||||
isBelowMdScreen={isBelowMdScreen}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
isBelowLgScreen={isBelowLgScreen}
|
||||
/>
|
||||
|
||||
<SendMsgForm
|
||||
dispatch={dispatch}
|
||||
activeUser={activeUser}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
messageInputRef={messageInputRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeUser && (
|
||||
<UserProfileRight
|
||||
open={userProfileRightOpen}
|
||||
handleClose={() => {
|
||||
setUserProfileRightOpen(false)
|
||||
setBackdropOpen(false)
|
||||
}}
|
||||
activeUser={activeUser}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
isBelowLgScreen={isBelowLgScreen}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatContent
|
||||
@@ -1,230 +0,0 @@
|
||||
// React Imports
|
||||
import { useRef, useEffect } from 'react'
|
||||
import type { MutableRefObject, ReactNode } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Avatar from '@mui/material/Avatar'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// Type Imports
|
||||
import type { ChatType, ChatDataType, UserChatType, ProfileUserType } from '@/types/apps/chatTypes'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
|
||||
// Util Imports
|
||||
import { getInitials } from '@/utils/getInitials'
|
||||
|
||||
type MsgGroupType = {
|
||||
senderId: number
|
||||
messages: Omit<UserChatType, 'senderId'>[]
|
||||
}
|
||||
|
||||
type ChatLogProps = {
|
||||
chatStore: ChatDataType
|
||||
isBelowLgScreen: boolean
|
||||
isBelowMdScreen: boolean
|
||||
isBelowSmScreen: boolean
|
||||
}
|
||||
|
||||
// Formats the chat data into a structured format for display.
|
||||
const formatedChatData = (chats: ChatType['chat'], profileUser: ProfileUserType) => {
|
||||
const formattedChatData: MsgGroupType[] = []
|
||||
let chatMessageSenderId = chats[0] ? chats[0].senderId : profileUser.id
|
||||
let msgGroup: MsgGroupType = {
|
||||
senderId: chatMessageSenderId,
|
||||
messages: []
|
||||
}
|
||||
|
||||
chats.forEach((chat, index) => {
|
||||
if (chatMessageSenderId === chat.senderId) {
|
||||
msgGroup.messages.push({
|
||||
time: chat.time,
|
||||
message: chat.message,
|
||||
msgStatus: chat.msgStatus
|
||||
})
|
||||
} else {
|
||||
chatMessageSenderId = chat.senderId
|
||||
|
||||
formattedChatData.push(msgGroup)
|
||||
msgGroup = {
|
||||
senderId: chat.senderId,
|
||||
messages: [
|
||||
{
|
||||
time: chat.time,
|
||||
message: chat.message,
|
||||
msgStatus: chat.msgStatus
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (index === chats.length - 1) formattedChatData.push(msgGroup)
|
||||
})
|
||||
|
||||
return formattedChatData
|
||||
}
|
||||
|
||||
// Wrapper for the chat log to handle scrolling
|
||||
const ScrollWrapper = ({
|
||||
children,
|
||||
isBelowLgScreen,
|
||||
scrollRef,
|
||||
className
|
||||
}: {
|
||||
children: ReactNode
|
||||
isBelowLgScreen: boolean
|
||||
scrollRef: MutableRefObject<null>
|
||||
className?: string
|
||||
}) => {
|
||||
if (isBelowLgScreen) {
|
||||
return (
|
||||
<div ref={scrollRef} className={classnames('bs-full overflow-y-auto overflow-x-hidden', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<PerfectScrollbar ref={scrollRef} options={{ wheelPropagation: false }} className={className}>
|
||||
{children}
|
||||
</PerfectScrollbar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const ChatLog = ({ chatStore, isBelowLgScreen, isBelowMdScreen, isBelowSmScreen }: ChatLogProps) => {
|
||||
// Props
|
||||
const { profileUser, contacts } = chatStore
|
||||
|
||||
// Vars
|
||||
const activeUserChat = chatStore.chats.find((chat: ChatType) => chat.userId === chatStore.activeUser?.id)
|
||||
|
||||
// Refs
|
||||
const scrollRef = useRef(null)
|
||||
|
||||
// Function to scroll to bottom when new message is sent
|
||||
const scrollToBottom = () => {
|
||||
if (scrollRef.current) {
|
||||
if (isBelowLgScreen) {
|
||||
// @ts-ignore
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
||||
} else {
|
||||
// @ts-ignore
|
||||
scrollRef.current._container.scrollTop = scrollRef.current._container.scrollHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to bottom on new message
|
||||
useEffect(() => {
|
||||
if (activeUserChat && activeUserChat.chat && activeUserChat.chat.length) {
|
||||
scrollToBottom()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [chatStore])
|
||||
|
||||
return (
|
||||
<ScrollWrapper isBelowLgScreen={isBelowLgScreen} scrollRef={scrollRef}>
|
||||
<CardContent className='p-0'>
|
||||
{activeUserChat &&
|
||||
formatedChatData(activeUserChat.chat, profileUser).map((msgGroup, index) => {
|
||||
const isSender = msgGroup.senderId === profileUser.id
|
||||
|
||||
return (
|
||||
<div key={index} className={classnames('flex gap-4 p-6', { 'flex-row-reverse': isSender })}>
|
||||
{!isSender ? (
|
||||
contacts.find(contact => contact.id === activeUserChat?.userId)?.avatar ? (
|
||||
<Avatar
|
||||
alt={contacts.find(contact => contact.id === activeUserChat?.userId)?.fullName}
|
||||
src={contacts.find(contact => contact.id === activeUserChat?.userId)?.avatar}
|
||||
className='is-8 bs-8'
|
||||
/>
|
||||
) : (
|
||||
<CustomAvatar
|
||||
color={contacts.find(contact => contact.id === activeUserChat?.userId)?.avatarColor}
|
||||
skin='light'
|
||||
size={32}
|
||||
>
|
||||
{getInitials(contacts.find(contact => contact.id === activeUserChat?.userId)?.fullName as string)}
|
||||
</CustomAvatar>
|
||||
)
|
||||
) : profileUser.avatar ? (
|
||||
<Avatar alt={profileUser.fullName} src={profileUser.avatar} className='is-8 bs-8' />
|
||||
) : (
|
||||
<CustomAvatar alt={profileUser.fullName} src={profileUser.avatar} size={32} />
|
||||
)}
|
||||
<div
|
||||
className={classnames('flex flex-col gap-2', {
|
||||
'items-end': isSender,
|
||||
'max-is-[65%]': !isBelowMdScreen,
|
||||
'max-is-[75%]': isBelowMdScreen && !isBelowSmScreen,
|
||||
'max-is-[calc(100%-5.75rem)]': isBelowSmScreen
|
||||
})}
|
||||
>
|
||||
{msgGroup.messages.map((msg, index) => (
|
||||
<Typography
|
||||
key={index}
|
||||
className={classnames('whitespace-pre-wrap pli-4 plb-2 shadow-xs', {
|
||||
'bg-backgroundPaper rounded-e rounded-b': !isSender,
|
||||
'bg-primary text-[var(--mui-palette-primary-contrastText)] rounded-s rounded-b': isSender
|
||||
})}
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
>
|
||||
{msg.message}
|
||||
</Typography>
|
||||
))}
|
||||
{msgGroup.messages.map(
|
||||
(msg, index) =>
|
||||
index === msgGroup.messages.length - 1 &&
|
||||
(isSender ? (
|
||||
<div key={index} className='flex items-center gap-2'>
|
||||
{msg.msgStatus?.isSeen ? (
|
||||
<i className='tabler-checks text-success text-base' />
|
||||
) : msg.msgStatus?.isDelivered ? (
|
||||
<i className='tabler-checks text-base' />
|
||||
) : (
|
||||
msg.msgStatus?.isSent && <i className='tabler-check text-base' />
|
||||
)}
|
||||
{index === activeUserChat.chat.length - 1 ? (
|
||||
<Typography variant='caption'>
|
||||
{new Date().toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true })}
|
||||
</Typography>
|
||||
) : msg.time ? (
|
||||
<Typography variant='caption'>
|
||||
{new Date(msg.time).toLocaleString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true
|
||||
})}
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
) : index === activeUserChat.chat.length - 1 ? (
|
||||
<Typography key={index} variant='caption'>
|
||||
{new Date().toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true })}
|
||||
</Typography>
|
||||
) : msg.time ? (
|
||||
<Typography key={index} variant='caption'>
|
||||
{new Date(msg.time).toLocaleString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true
|
||||
})}
|
||||
</Typography>
|
||||
) : null)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</ScrollWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatLog
|
||||
@@ -1,241 +0,0 @@
|
||||
// React Imports
|
||||
import { useRef, useState, useEffect } from 'react'
|
||||
import type { FormEvent, KeyboardEvent, RefObject, MouseEvent } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Button from '@mui/material/Button'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Popper from '@mui/material/Popper'
|
||||
import Fade from '@mui/material/Fade'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener'
|
||||
import Menu from '@mui/material/Menu'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
|
||||
// Third-party Imports
|
||||
import Picker from '@emoji-mart/react'
|
||||
import data from '@emoji-mart/data'
|
||||
|
||||
// Type Imports
|
||||
import type { ContactType } from '@/types/apps/chatTypes'
|
||||
import type { AppDispatch } from '@/redux-store'
|
||||
|
||||
// Slice Imports
|
||||
import { sendMsg } from '@/redux-store/slices/chat'
|
||||
|
||||
// Component Imports
|
||||
import CustomIconButton from '@core/components/mui/IconButton'
|
||||
|
||||
type Props = {
|
||||
dispatch: AppDispatch
|
||||
activeUser: ContactType
|
||||
isBelowSmScreen: boolean
|
||||
messageInputRef: RefObject<HTMLDivElement>
|
||||
}
|
||||
|
||||
// Emoji Picker Component for selecting emojis
|
||||
const EmojiPicker = ({
|
||||
onChange,
|
||||
isBelowSmScreen,
|
||||
openEmojiPicker,
|
||||
setOpenEmojiPicker,
|
||||
anchorRef
|
||||
}: {
|
||||
onChange: (value: string) => void
|
||||
isBelowSmScreen: boolean
|
||||
openEmojiPicker: boolean
|
||||
setOpenEmojiPicker: (value: boolean | ((prevVar: boolean) => boolean)) => void
|
||||
anchorRef: RefObject<HTMLButtonElement>
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Popper
|
||||
open={openEmojiPicker}
|
||||
transition
|
||||
disablePortal
|
||||
placement='top-start'
|
||||
className='z-[12]'
|
||||
anchorEl={anchorRef.current}
|
||||
>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Fade {...TransitionProps} style={{ transformOrigin: placement === 'top-start' ? 'right top' : 'left top' }}>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={() => setOpenEmojiPicker(false)}>
|
||||
<span>
|
||||
<Picker
|
||||
emojiSize={18}
|
||||
theme='light'
|
||||
data={data}
|
||||
maxFrequentRows={1}
|
||||
onEmojiSelect={(emoji: any) => {
|
||||
onChange(emoji.native)
|
||||
setOpenEmojiPicker(false)
|
||||
}}
|
||||
{...(isBelowSmScreen && { perLine: 8 })}
|
||||
/>
|
||||
</span>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Fade>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SendMsgForm = ({ dispatch, activeUser, isBelowSmScreen, messageInputRef }: Props) => {
|
||||
// States
|
||||
const [msg, setMsg] = useState('')
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
const [openEmojiPicker, setOpenEmojiPicker] = useState(false)
|
||||
|
||||
// Refs
|
||||
const anchorRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
// Vars
|
||||
const open = Boolean(anchorEl)
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpenEmojiPicker(prevOpen => !prevOpen)
|
||||
}
|
||||
|
||||
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(prev => (prev ? null : event.currentTarget))
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleSendMsg = (event: FormEvent | KeyboardEvent, msg: string) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (msg.trim() !== '') {
|
||||
dispatch(sendMsg({ msg }))
|
||||
setMsg('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputEndAdornment = () => {
|
||||
return (
|
||||
<div className='flex items-center gap-1'>
|
||||
{isBelowSmScreen ? (
|
||||
<>
|
||||
<IconButton
|
||||
id='option-menu'
|
||||
aria-haspopup='true'
|
||||
{...(open && { 'aria-expanded': true, 'aria-controls': 'share-menu' })}
|
||||
onClick={handleClick}
|
||||
ref={anchorRef}
|
||||
>
|
||||
<i className='tabler-dots-vertical text-textPrimary' />
|
||||
</IconButton>
|
||||
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleToggle()
|
||||
handleClose()
|
||||
}}
|
||||
>
|
||||
<i className='tabler-mood-smile' />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<i className='tabler-microphone' />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose} className='p-0'>
|
||||
<label htmlFor='upload-img' className='plb-2 pli-4'>
|
||||
<i className='tabler-paperclip' />
|
||||
<input hidden type='file' id='upload-img' />
|
||||
</label>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<EmojiPicker
|
||||
anchorRef={anchorRef}
|
||||
openEmojiPicker={openEmojiPicker}
|
||||
setOpenEmojiPicker={setOpenEmojiPicker}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
onChange={value => {
|
||||
setMsg(msg + value)
|
||||
|
||||
if (messageInputRef.current) {
|
||||
messageInputRef.current.focus()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconButton ref={anchorRef} onClick={handleToggle}>
|
||||
<i className='tabler-mood-smile cursor-pointer text-textPrimary' />
|
||||
</IconButton>
|
||||
<EmojiPicker
|
||||
anchorRef={anchorRef}
|
||||
openEmojiPicker={openEmojiPicker}
|
||||
setOpenEmojiPicker={setOpenEmojiPicker}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
onChange={value => {
|
||||
setMsg(msg + value)
|
||||
|
||||
if (messageInputRef.current) {
|
||||
messageInputRef.current.focus()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton>
|
||||
<i className='tabler-microphone text-textPrimary' />
|
||||
</IconButton>
|
||||
<IconButton component='label' htmlFor='upload-img'>
|
||||
<i className='tabler-paperclip text-textPrimary' />
|
||||
<input hidden type='file' id='upload-img' />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
{isBelowSmScreen ? (
|
||||
<CustomIconButton variant='contained' color='primary' type='submit'>
|
||||
<i className='tabler-send' />
|
||||
</CustomIconButton>
|
||||
) : (
|
||||
<Button variant='contained' color='primary' type='submit' endIcon={<i className='tabler-send' />}>
|
||||
Send
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setMsg('')
|
||||
}, [activeUser.id])
|
||||
|
||||
return (
|
||||
<form autoComplete='off' onSubmit={event => handleSendMsg(event, msg)}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
maxRows={4}
|
||||
placeholder='Type a message'
|
||||
value={msg}
|
||||
className='p-6'
|
||||
onChange={e => setMsg(e.target.value)}
|
||||
sx={{
|
||||
'& fieldset': { border: '0' },
|
||||
'& .MuiOutlinedInput-root': {
|
||||
background: 'var(--mui-palette-background-paper)',
|
||||
boxShadow: 'var(--mui-customShadows-xs) !important'
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
handleSendMsg(e, msg)
|
||||
}
|
||||
}}
|
||||
size='small'
|
||||
inputRef={messageInputRef}
|
||||
slotProps={{ input: { endAdornment: handleInputEndAdornment() } }}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default SendMsgForm
|
||||
@@ -1,293 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { ReactNode, RefObject } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Avatar from '@mui/material/Avatar'
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Autocomplete from '@mui/material/Autocomplete'
|
||||
import InputAdornment from '@mui/material/InputAdornment'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// Type Imports
|
||||
import type { ThemeColor } from '@core/types'
|
||||
import type { ChatDataType, StatusObjType } from '@/types/apps/chatTypes'
|
||||
import type { AppDispatch } from '@/redux-store'
|
||||
|
||||
// Slice Imports
|
||||
import { addNewChat } from '@/redux-store/slices/chat'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import CustomChip from '@core/components/mui/Chip'
|
||||
import UserProfileLeft from './UserProfileLeft'
|
||||
import AvatarWithBadge from './AvatarWithBadge'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Util Imports
|
||||
import { getInitials } from '@/utils/getInitials'
|
||||
import { formatDateToMonthShort } from './utils'
|
||||
|
||||
export const statusObj: StatusObjType = {
|
||||
busy: 'error',
|
||||
away: 'warning',
|
||||
online: 'success',
|
||||
offline: 'secondary'
|
||||
}
|
||||
|
||||
type Props = {
|
||||
chatStore: ChatDataType
|
||||
getActiveUserData: (id: number) => void
|
||||
dispatch: AppDispatch
|
||||
backdropOpen: boolean
|
||||
setBackdropOpen: (value: boolean) => void
|
||||
sidebarOpen: boolean
|
||||
setSidebarOpen: (value: boolean) => void
|
||||
isBelowLgScreen: boolean
|
||||
isBelowMdScreen: boolean
|
||||
isBelowSmScreen: boolean
|
||||
messageInputRef: RefObject<HTMLDivElement>
|
||||
}
|
||||
|
||||
type RenderChatType = {
|
||||
chatStore: ChatDataType
|
||||
getActiveUserData: (id: number) => void
|
||||
setSidebarOpen: (value: boolean) => void
|
||||
backdropOpen: boolean
|
||||
setBackdropOpen: (value: boolean) => void
|
||||
isBelowMdScreen: boolean
|
||||
}
|
||||
|
||||
// Render chat list
|
||||
const renderChat = (props: RenderChatType) => {
|
||||
// Props
|
||||
const { chatStore, getActiveUserData, setSidebarOpen, backdropOpen, setBackdropOpen, isBelowMdScreen } = props
|
||||
|
||||
return chatStore.chats.map(chat => {
|
||||
const contact = chatStore.contacts.find(contact => contact.id === chat.userId) || chatStore.contacts[0]
|
||||
const isChatActive = chatStore.activeUser?.id === contact.id
|
||||
|
||||
return (
|
||||
<li
|
||||
key={chat.id}
|
||||
className={classnames('flex items-start gap-4 pli-3 plb-2 cursor-pointer rounded mbe-1', {
|
||||
'bg-primary shadow-primarySm': isChatActive,
|
||||
'text-[var(--mui-palette-primary-contrastText)]': isChatActive
|
||||
})}
|
||||
onClick={() => {
|
||||
getActiveUserData(chat.userId)
|
||||
isBelowMdScreen && setSidebarOpen(false)
|
||||
isBelowMdScreen && backdropOpen && setBackdropOpen(false)
|
||||
}}
|
||||
>
|
||||
<AvatarWithBadge
|
||||
src={contact.avatar}
|
||||
isChatActive={isChatActive}
|
||||
alt={contact.fullName}
|
||||
badgeColor={statusObj[contact.status]}
|
||||
color={contact.avatarColor}
|
||||
/>
|
||||
<div className='min-is-0 flex-auto'>
|
||||
<Typography color='inherit'>{contact?.fullName}</Typography>
|
||||
{chat.chat.length ? (
|
||||
<Typography variant='body2' color={isChatActive ? 'inherit' : 'text.secondary'} className='truncate'>
|
||||
{chat.chat[chat.chat.length - 1].message}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant='body2' color={isChatActive ? 'inherit' : 'text.secondary'} className='truncate'>
|
||||
{contact.role}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-col items-end justify-start'>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='inherit'
|
||||
className={classnames('truncate', {
|
||||
'text-textDisabled': !isChatActive
|
||||
})}
|
||||
>
|
||||
{chat.chat.length ? formatDateToMonthShort(chat.chat[chat.chat.length - 1].time) : null}
|
||||
</Typography>
|
||||
{chat.unseenMsgs > 0 ? <CustomChip round='true' label={chat.unseenMsgs} color='error' size='small' /> : null}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Scroll wrapper for chat list
|
||||
const ScrollWrapper = ({ children, isBelowLgScreen }: { children: ReactNode; isBelowLgScreen: boolean }) => {
|
||||
if (isBelowLgScreen) {
|
||||
return <div className='bs-full overflow-y-auto overflow-x-hidden'>{children}</div>
|
||||
} else {
|
||||
return <PerfectScrollbar options={{ wheelPropagation: false }}>{children}</PerfectScrollbar>
|
||||
}
|
||||
}
|
||||
|
||||
const SidebarLeft = (props: Props) => {
|
||||
// Props
|
||||
const {
|
||||
chatStore,
|
||||
getActiveUserData,
|
||||
dispatch,
|
||||
backdropOpen,
|
||||
setBackdropOpen,
|
||||
sidebarOpen,
|
||||
setSidebarOpen,
|
||||
isBelowLgScreen,
|
||||
isBelowMdScreen,
|
||||
isBelowSmScreen,
|
||||
messageInputRef
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [userSidebar, setUserSidebar] = useState(false)
|
||||
const [searchValue, setSearchValue] = useState<string | null>()
|
||||
|
||||
const handleChange = (event: any, newValue: string | null) => {
|
||||
setSearchValue(newValue)
|
||||
dispatch(addNewChat({ id: chatStore.contacts.find(contact => contact.fullName === newValue)?.id }))
|
||||
getActiveUserData(
|
||||
chatStore.contacts.find(contact => contact.fullName === newValue)?.id || (chatStore.activeUser?.id as number)
|
||||
)
|
||||
isBelowMdScreen && setSidebarOpen(false)
|
||||
setBackdropOpen(false)
|
||||
setSearchValue(null)
|
||||
messageInputRef.current?.focus()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
open={sidebarOpen}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
className='bs-full'
|
||||
variant={!isBelowMdScreen ? 'permanent' : 'persistent'}
|
||||
ModalProps={{
|
||||
disablePortal: true,
|
||||
keepMounted: true // Better open performance on mobile.
|
||||
}}
|
||||
sx={{
|
||||
zIndex: isBelowMdScreen && sidebarOpen ? 11 : 10,
|
||||
position: !isBelowMdScreen ? 'static' : 'absolute',
|
||||
...(isBelowSmScreen && sidebarOpen && { width: '100%' }),
|
||||
'& .MuiDrawer-paper': {
|
||||
overflow: 'hidden',
|
||||
boxShadow: 'none',
|
||||
width: isBelowSmScreen ? '100%' : '370px',
|
||||
position: !isBelowMdScreen ? 'static' : 'absolute'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center plb-[18px] pli-6 gap-4 border-be'>
|
||||
<AvatarWithBadge
|
||||
alt={chatStore.profileUser.fullName}
|
||||
src={chatStore.profileUser.avatar}
|
||||
badgeColor={statusObj[chatStore.profileUser.status]}
|
||||
onClick={() => {
|
||||
setUserSidebar(true)
|
||||
}}
|
||||
/>
|
||||
<div className='flex is-full items-center flex-auto sm:gap-x-3'>
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
size='small'
|
||||
id='select-contact'
|
||||
options={chatStore.contacts.map(contact => contact.fullName) || []}
|
||||
value={searchValue || null}
|
||||
onChange={handleChange}
|
||||
renderInput={params => (
|
||||
<CustomTextField
|
||||
{...params}
|
||||
variant='outlined'
|
||||
placeholder='Search Contacts'
|
||||
slotProps={{
|
||||
input: {
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<InputAdornment position='start'>
|
||||
<i className='tabler-search' />
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
renderOption={(props, option) => {
|
||||
const contact = chatStore.contacts.find(contact => contact.fullName === option)
|
||||
|
||||
return (
|
||||
<li
|
||||
{...props}
|
||||
key={option.toLowerCase().replace(/\s+/g, '-')}
|
||||
className={classnames('gap-3 max-sm:pli-3', props.className)}
|
||||
>
|
||||
{contact ? (
|
||||
contact.avatar ? (
|
||||
<Avatar
|
||||
alt={contact.fullName}
|
||||
src={contact.avatar}
|
||||
key={option.toLowerCase().replace(/\s+/g, '-')}
|
||||
/>
|
||||
) : (
|
||||
<CustomAvatar
|
||||
color={contact.avatarColor as ThemeColor}
|
||||
skin='light'
|
||||
key={option.toLowerCase().replace(/\s+/g, '-')}
|
||||
>
|
||||
{getInitials(contact.fullName)}
|
||||
</CustomAvatar>
|
||||
)
|
||||
) : null}
|
||||
{option}
|
||||
</li>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{isBelowMdScreen ? (
|
||||
<IconButton
|
||||
className='mis-2'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
setSidebarOpen(false)
|
||||
setBackdropOpen(false)
|
||||
}}
|
||||
>
|
||||
<i className='tabler-x text-2xl' />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<ScrollWrapper isBelowLgScreen={isBelowLgScreen}>
|
||||
<ul className='p-3 pbs-4'>
|
||||
{renderChat({
|
||||
chatStore,
|
||||
getActiveUserData,
|
||||
backdropOpen,
|
||||
setSidebarOpen,
|
||||
isBelowMdScreen,
|
||||
setBackdropOpen
|
||||
})}
|
||||
</ul>
|
||||
</ScrollWrapper>
|
||||
</Drawer>
|
||||
|
||||
<UserProfileLeft
|
||||
userSidebar={userSidebar}
|
||||
setUserSidebar={setUserSidebar}
|
||||
profileUserData={chatStore.profileUser}
|
||||
dispatch={dispatch}
|
||||
isBelowLgScreen={isBelowLgScreen}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SidebarLeft
|
||||
@@ -1,183 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { ChangeEvent, ReactNode } from 'react'
|
||||
|
||||
// MUI Import
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Radio from '@mui/material/Radio'
|
||||
import RadioGroup from '@mui/material/RadioGroup'
|
||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||
import FormLabel from '@mui/material/FormLabel'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import ListItemButton from '@mui/material/ListItemButton'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||
import Switch from '@mui/material/Switch'
|
||||
import Backdrop from '@mui/material/Backdrop'
|
||||
import Button from '@mui/material/Button'
|
||||
|
||||
// Third Party Imports
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// Type Imports
|
||||
import type { AppDispatch } from '@/redux-store'
|
||||
import type { ProfileUserType, StatusType } from '@/types/apps/chatTypes'
|
||||
|
||||
// Slice Imports
|
||||
import { setUserStatus } from '@/redux-store/slices/chat'
|
||||
|
||||
// Component Imports
|
||||
import AvatarWithBadge from './AvatarWithBadge'
|
||||
import { statusObj } from '@views/apps/chat/SidebarLeft'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
type Props = {
|
||||
userSidebar: boolean
|
||||
setUserSidebar: (open: boolean) => void
|
||||
profileUserData: ProfileUserType
|
||||
dispatch: AppDispatch
|
||||
isBelowLgScreen: boolean
|
||||
isBelowSmScreen: boolean
|
||||
}
|
||||
|
||||
const ScrollWrapper = ({ children, isBelowLgScreen }: { children: ReactNode; isBelowLgScreen: boolean }) => {
|
||||
if (isBelowLgScreen) {
|
||||
return <div className='bs-full overflow-x-hidden overflow-y-auto'>{children}</div>
|
||||
} else {
|
||||
return <PerfectScrollbar options={{ wheelPropagation: false }}>{children}</PerfectScrollbar>
|
||||
}
|
||||
}
|
||||
|
||||
const UserProfileLeft = (props: Props) => {
|
||||
// Props
|
||||
const { userSidebar, setUserSidebar, profileUserData, dispatch, isBelowLgScreen, isBelowSmScreen } = props
|
||||
|
||||
// States
|
||||
const [twoStepVerification, setTwoStepVerification] = useState<boolean>(true)
|
||||
const [notification, setNotification] = useState<boolean>(false)
|
||||
|
||||
const handleTwoStepVerification = () => {
|
||||
setTwoStepVerification(!twoStepVerification)
|
||||
}
|
||||
|
||||
const handleNotification = () => {
|
||||
setNotification(!notification)
|
||||
}
|
||||
|
||||
const handleUserStatus = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setUserStatus({ status: e.target.value as StatusType }))
|
||||
}
|
||||
|
||||
return profileUserData ? (
|
||||
<>
|
||||
<Drawer
|
||||
open={userSidebar}
|
||||
anchor='left'
|
||||
variant='persistent'
|
||||
ModalProps={{ keepMounted: true }}
|
||||
onClose={() => setUserSidebar(false)}
|
||||
sx={{
|
||||
zIndex: 13,
|
||||
'& .MuiDrawer-paper': { width: isBelowSmScreen ? '100%' : '370px', position: 'absolute', border: 0 }
|
||||
}}
|
||||
>
|
||||
<IconButton className='absolute block-start-4 inline-end-4' onClick={() => setUserSidebar(false)}>
|
||||
<i className='tabler-x text-2xl' />
|
||||
</IconButton>
|
||||
<div className='flex flex-col justify-center items-center gap-4 mbs-6 pli-6 pbs-6 pbe-3'>
|
||||
<AvatarWithBadge
|
||||
alt={profileUserData.fullName}
|
||||
src={profileUserData.avatar}
|
||||
badgeColor={statusObj[profileUserData.status]}
|
||||
className='bs-[84px] is-[84px]'
|
||||
badgeSize={12}
|
||||
/>
|
||||
<div className='text-center'>
|
||||
<Typography variant='h5'>{profileUserData.fullName}</Typography>
|
||||
<Typography>{profileUserData.role}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollWrapper isBelowLgScreen={isBelowLgScreen}>
|
||||
<div className='flex flex-col gap-6 p-6 pbs-3'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography className='uppercase' color='text.disabled'>
|
||||
About
|
||||
</Typography>
|
||||
<CustomTextField fullWidth rows={3} multiline id='about-textarea' defaultValue={profileUserData.about} />
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<FormLabel id='status-radio-buttons-group-label' className='uppercase text-textDisabled'>
|
||||
Status
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
value={profileUserData.status}
|
||||
name='radio-buttons-group'
|
||||
onChange={handleUserStatus}
|
||||
aria-labelledby='status-radio-buttons-group-label'
|
||||
>
|
||||
<FormControlLabel value='online' control={<Radio color='success' />} label='Online' />
|
||||
<FormControlLabel value='away' control={<Radio color='warning' />} label='Away' />
|
||||
<FormControlLabel value='busy' control={<Radio color='error' />} label='Do not disturb' />
|
||||
<FormControlLabel value='offline' control={<Radio color='secondary' />} label='Offline' />
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography className='uppercase' color='text.disabled'>
|
||||
Settings
|
||||
</Typography>
|
||||
<List className='plb-0'>
|
||||
<ListItem
|
||||
disablePadding
|
||||
secondaryAction={<Switch checked={twoStepVerification} onChange={handleTwoStepVerification} />}
|
||||
>
|
||||
<ListItemButton onClick={handleTwoStepVerification} className='p-2'>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-lock' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Two-step Verification' />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
disablePadding
|
||||
secondaryAction={<Switch checked={notification} onChange={handleNotification} />}
|
||||
>
|
||||
<ListItemButton onClick={handleNotification} className='p-2'>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-bell' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Notification' />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton className='p-2'>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-user-plus' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Invite Friends' />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton className='p-2'>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-trash' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Delete Account' />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
<Button variant='contained' fullWidth className='mbs-auto' endIcon={<i className='tabler-logout' />}>
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</ScrollWrapper>
|
||||
</Drawer>
|
||||
<Backdrop open={userSidebar} onClick={() => setUserSidebar(false)} className='absolute z-[12]' />
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default UserProfileLeft
|
||||
@@ -1,172 +0,0 @@
|
||||
// React Imports
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import ListItemButton from '@mui/material/ListItemButton'
|
||||
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import Button from '@mui/material/Button'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// Type Imports
|
||||
import type { ContactType } from '@/types/apps/chatTypes'
|
||||
|
||||
// Component Imports
|
||||
import { statusObj } from './SidebarLeft'
|
||||
import AvatarWithBadge from './AvatarWithBadge'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
handleClose: () => void
|
||||
activeUser: ContactType
|
||||
isBelowLgScreen: boolean
|
||||
isBelowSmScreen: boolean
|
||||
}
|
||||
|
||||
const ScrollWrapper = ({
|
||||
children,
|
||||
isBelowLgScreen,
|
||||
className
|
||||
}: {
|
||||
children: ReactNode
|
||||
isBelowLgScreen: boolean
|
||||
className?: string
|
||||
}) => {
|
||||
if (isBelowLgScreen) {
|
||||
return <div className={classnames('bs-full overflow-x-hidden overflow-y-auto', className)}>{children}</div>
|
||||
} else {
|
||||
return (
|
||||
<PerfectScrollbar options={{ wheelPropagation: false }} className={className}>
|
||||
{children}
|
||||
</PerfectScrollbar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const UserProfileRight = (props: Props) => {
|
||||
// Props
|
||||
const { open, handleClose, activeUser, isBelowLgScreen, isBelowSmScreen } = props
|
||||
|
||||
return activeUser ? (
|
||||
<Drawer
|
||||
open={open}
|
||||
anchor='right'
|
||||
variant='persistent'
|
||||
ModalProps={{ keepMounted: true }}
|
||||
sx={{
|
||||
zIndex: 12,
|
||||
'& .MuiDrawer-paper': { width: isBelowSmScreen ? '100%' : '370px', position: 'absolute', border: 0 }
|
||||
}}
|
||||
>
|
||||
<IconButton className='absolute block-start-4 inline-end-4' onClick={handleClose}>
|
||||
<i className='tabler-x text-2xl' />
|
||||
</IconButton>
|
||||
<div className='flex flex-col justify-center items-center gap-4 mbs-6 pli-6 pbs-6 pbe-3'>
|
||||
<AvatarWithBadge
|
||||
alt={activeUser.fullName}
|
||||
src={activeUser.avatar}
|
||||
color={activeUser.avatarColor}
|
||||
badgeColor={statusObj[activeUser.status]}
|
||||
className='bs-[84px] is-[84px] text-3xl'
|
||||
badgeSize={12}
|
||||
/>
|
||||
<div className='text-center'>
|
||||
<Typography variant='h5'>{activeUser.fullName}</Typography>
|
||||
<Typography>{activeUser.role}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollWrapper isBelowLgScreen={isBelowLgScreen} className='flex flex-col gap-6 p-6 pbs-3'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography className='uppercase' color='text.disabled'>
|
||||
About
|
||||
</Typography>
|
||||
<Typography>{activeUser.about}</Typography>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography className='uppercase' color='text.disabled'>
|
||||
Personal Information
|
||||
</Typography>
|
||||
<List className='plb-0'>
|
||||
<ListItem className='p-2 gap-2'>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-mail' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={`${activeUser.fullName.toLowerCase().replace(/\s/g, '_')}@email.com`} />
|
||||
</ListItem>
|
||||
<ListItem className='p-2 gap-2'>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-phone' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='+1(123) 456 - 7890' />
|
||||
</ListItem>
|
||||
<ListItem className='p-2 gap-2'>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-clock' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Mon - Fri 10AM - 8PM' />
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography className='uppercase' color='text.disabled'>
|
||||
Options
|
||||
</Typography>
|
||||
<List className='plb-0'>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton className='p-2'>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-bookmark' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Add Tag' />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton className='p-2'>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-star' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Important Contact' />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton className='p-2'>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-photo' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Shared Image' />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton className='p-2'>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-circle-off' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Block Contact' />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='error'
|
||||
fullWidth
|
||||
className='mbs-auto'
|
||||
endIcon={<i className='tabler-trash' />}
|
||||
>
|
||||
Delete Contact
|
||||
</Button>
|
||||
</ScrollWrapper>
|
||||
</Drawer>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default UserProfileRight
|
||||
@@ -1,121 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Backdrop from '@mui/material/Backdrop'
|
||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||
import type { Theme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import classNames from 'classnames'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
// Type Imports
|
||||
import type { RootState } from '@/redux-store'
|
||||
|
||||
// Slice Imports
|
||||
import { getActiveUserData } from '@/redux-store/slices/chat'
|
||||
|
||||
// Component Imports
|
||||
import SidebarLeft from './SidebarLeft'
|
||||
import ChatContent from './ChatContent'
|
||||
|
||||
// Hook Imports
|
||||
import { useSettings } from '@core/hooks/useSettings'
|
||||
|
||||
// Util Imports
|
||||
import { commonLayoutClasses } from '@layouts/utils/layoutClasses'
|
||||
|
||||
const ChatWrapper = () => {
|
||||
// States
|
||||
const [backdropOpen, setBackdropOpen] = useState(false)
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
|
||||
// Refs
|
||||
const messageInputRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Hooks
|
||||
const { settings } = useSettings()
|
||||
const dispatch = useDispatch()
|
||||
const chatStore = useSelector((state: RootState) => state.chatReducer)
|
||||
const isBelowLgScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('lg'))
|
||||
const isBelowMdScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'))
|
||||
const isBelowSmScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
|
||||
|
||||
// Get active user’s data
|
||||
const activeUser = (id: number) => {
|
||||
dispatch(getActiveUserData(id))
|
||||
}
|
||||
|
||||
// Focus on message input when active user changes
|
||||
useEffect(() => {
|
||||
if (chatStore.activeUser?.id !== null && messageInputRef.current) {
|
||||
messageInputRef.current.focus()
|
||||
}
|
||||
}, [chatStore.activeUser])
|
||||
|
||||
// Close backdrop when sidebar is open on below md screen
|
||||
useEffect(() => {
|
||||
if (!isBelowMdScreen && backdropOpen && sidebarOpen) {
|
||||
setBackdropOpen(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isBelowMdScreen])
|
||||
|
||||
// Open backdrop when sidebar is open on below sm screen
|
||||
useEffect(() => {
|
||||
if (!isBelowSmScreen && sidebarOpen) {
|
||||
setBackdropOpen(true)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isBelowSmScreen])
|
||||
|
||||
// Close sidebar when backdrop is closed on below md screen
|
||||
useEffect(() => {
|
||||
if (!backdropOpen && sidebarOpen) {
|
||||
setSidebarOpen(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [backdropOpen])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(commonLayoutClasses.contentHeightFixed, 'flex is-full overflow-hidden rounded relative', {
|
||||
border: settings.skin === 'bordered',
|
||||
'shadow-md': settings.skin !== 'bordered'
|
||||
})}
|
||||
>
|
||||
<SidebarLeft
|
||||
chatStore={chatStore}
|
||||
getActiveUserData={activeUser}
|
||||
dispatch={dispatch}
|
||||
backdropOpen={backdropOpen}
|
||||
setBackdropOpen={setBackdropOpen}
|
||||
sidebarOpen={sidebarOpen}
|
||||
setSidebarOpen={setSidebarOpen}
|
||||
isBelowLgScreen={isBelowLgScreen}
|
||||
isBelowMdScreen={isBelowMdScreen}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
messageInputRef={messageInputRef}
|
||||
/>
|
||||
|
||||
<ChatContent
|
||||
chatStore={chatStore}
|
||||
dispatch={dispatch}
|
||||
backdropOpen={backdropOpen}
|
||||
setBackdropOpen={setBackdropOpen}
|
||||
setSidebarOpen={setSidebarOpen}
|
||||
isBelowMdScreen={isBelowMdScreen}
|
||||
isBelowLgScreen={isBelowLgScreen}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
messageInputRef={messageInputRef}
|
||||
/>
|
||||
|
||||
<Backdrop open={backdropOpen} onClick={() => setBackdropOpen(false)} className='absolute z-10' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatWrapper
|
||||
@@ -1,20 +0,0 @@
|
||||
const isToday = (date: Date | string) => {
|
||||
const today = new Date()
|
||||
|
||||
return (
|
||||
new Date(date).getDate() === today.getDate() &&
|
||||
new Date(date).getMonth() === today.getMonth() &&
|
||||
new Date(date).getFullYear() === today.getFullYear()
|
||||
)
|
||||
}
|
||||
|
||||
export const formatDateToMonthShort = (value: Date | string, toTimeForCurrentDay = true) => {
|
||||
const date = new Date(value)
|
||||
let formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' }
|
||||
|
||||
if (toTimeForCurrentDay && isToday(date)) {
|
||||
formatting = { hour: 'numeric', minute: 'numeric' }
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value))
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// MUI Imports
|
||||
import Button from '@mui/material/Button'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import type { ButtonProps } from '@mui/material/Button'
|
||||
|
||||
// Type Imports
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Component Imports
|
||||
import ConfirmationDialog from '@components/dialogs/confirmation-dialog'
|
||||
import OpenDialogOnElementClick from '@components/dialogs/OpenDialogOnElementClick'
|
||||
|
||||
const CustomerDetailHeader = ({ customerId }: { customerId: string }) => {
|
||||
// Vars
|
||||
const buttonProps = (children: string, color: ThemeColor, variant: ButtonProps['variant']): ButtonProps => ({
|
||||
children,
|
||||
color,
|
||||
variant
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap justify-between max-sm:flex-col sm:items-center gap-x-6 gap-y-4'>
|
||||
<div className='flex flex-col items-start gap-1'>
|
||||
<Typography variant='h4'>{`Customer ID #${customerId}`}</Typography>
|
||||
<Typography>Aug 17, 2020, 5:48 (ET)</Typography>
|
||||
</div>
|
||||
<OpenDialogOnElementClick
|
||||
element={Button}
|
||||
elementProps={buttonProps('Delete Customer', 'error', 'tonal')}
|
||||
dialog={ConfirmationDialog}
|
||||
dialogProps={{ type: 'delete-customer' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerDetailHeader
|
||||
@@ -1,99 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Button from '@mui/material/Button'
|
||||
import type { ButtonProps } from '@mui/material/Button'
|
||||
|
||||
// Type Imports
|
||||
import type { Customer } from '@/types/apps/ecommerceTypes'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import EditUserInfo from '@components/dialogs/edit-user-info'
|
||||
import OpenDialogOnElementClick from '@components/dialogs/OpenDialogOnElementClick'
|
||||
|
||||
const CustomerDetails = ({ customerData }: { customerData?: Customer }) => {
|
||||
// Vars
|
||||
const buttonProps: ButtonProps = {
|
||||
variant: 'contained',
|
||||
children: 'Edit Details'
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className='flex flex-col pbs-12 gap-6'>
|
||||
<div className='flex flex-col justify-self-center items-center gap-6'>
|
||||
<div className='flex flex-col items-center gap-4'>
|
||||
<CustomAvatar src={customerData?.avatar} variant='rounded' alt='Customer Avatar' size={120} />
|
||||
<div className='flex flex-col items-center text-center'>
|
||||
<Typography variant='h5'>{customerData?.customer}</Typography>
|
||||
<Typography>Customer ID #{customerData?.customerId}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-around gap-4 flex-wrap is-full'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<CustomAvatar variant='rounded' skin='light' color='primary'>
|
||||
<i className='tabler-shopping-cart' />
|
||||
</CustomAvatar>
|
||||
<div>
|
||||
<Typography variant='h5'>{customerData?.order}</Typography>
|
||||
<Typography>Orders</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<CustomAvatar variant='rounded' skin='light' color='primary'>
|
||||
<i className='tabler-currency-dollar' />
|
||||
</CustomAvatar>
|
||||
<div>
|
||||
<Typography variant='h5'>${customerData?.totalSpent}</Typography>
|
||||
<Typography>Spent</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Typography variant='h5'>Details</Typography>
|
||||
<Divider />
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
Username:
|
||||
</Typography>
|
||||
<Typography>{customerData?.customer}</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
Billing Email:
|
||||
</Typography>
|
||||
<Typography>{customerData?.email}</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
Status:
|
||||
</Typography>
|
||||
<Chip label='Active' variant='tonal' color='success' size='small' />
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
Contact:
|
||||
</Typography>
|
||||
<Typography>+1 (234) 464-0600</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
Country:
|
||||
</Typography>
|
||||
<Typography>{customerData?.country}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<OpenDialogOnElementClick element={Button} elementProps={buttonProps} dialog={EditUserInfo} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerDetails
|
||||
@@ -1,45 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import MuiButton from '@mui/material/Button'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import type { ButtonProps } from '@mui/material/Button'
|
||||
|
||||
// Component Imports
|
||||
import UpgradePlan from '@components/dialogs/upgrade-plan'
|
||||
import OpenDialogOnElementClick from '@components/dialogs/OpenDialogOnElementClick'
|
||||
|
||||
const Button = styled(MuiButton)<ButtonProps>(() => ({
|
||||
backgroundColor: 'var(--mui-palette-common-white) !important',
|
||||
color: 'var(--mui-palette-primary-main) !important'
|
||||
}))
|
||||
|
||||
const CustomerPlan = () => {
|
||||
// Vars
|
||||
const buttonProps: ButtonProps = {
|
||||
variant: 'contained',
|
||||
children: 'Upgrade To Premium'
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className='flex flex-col gap-6 bg-gradient-to-tr from-primary to-[#9E95F5]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Typography variant='h5' color='common.white'>
|
||||
Upgrade to premium
|
||||
</Typography>
|
||||
<Typography color='common.white'>Upgrade customer to premium membership to access pro features.</Typography>
|
||||
</div>
|
||||
<img src='/images/apps/ecommerce/3d-rocket.png' className='-mis-7 -mbe-7' />
|
||||
</div>
|
||||
<OpenDialogOnElementClick element={Button} elementProps={buttonProps} dialog={UpgradePlan} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerPlan
|
||||
@@ -1,24 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Type Imports
|
||||
import type { Customer } from '@/types/apps/ecommerceTypes'
|
||||
|
||||
// Component Imports
|
||||
import CustomerDetails from './CustomerDetails'
|
||||
import CustomerPlan from './CustomerPlan'
|
||||
|
||||
const CustomerLeftOverview = ({ customerData }: { customerData?: Customer }) => {
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CustomerDetails customerData={customerData} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CustomerPlan />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerLeftOverview
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Collapse from '@mui/material/Collapse'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Button from '@mui/material/Button'
|
||||
import type { ButtonProps } from '@mui/material/Button'
|
||||
import type { IconButtonProps } from '@mui/material/IconButton'
|
||||
|
||||
// Component Imports
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
import AddNewAddress from '@components/dialogs/add-edit-address'
|
||||
import OpenDialogOnElementClick from '@components/dialogs/OpenDialogOnElementClick'
|
||||
|
||||
type propsType = {
|
||||
typeOfAddress: string
|
||||
isDefaultAddress: boolean
|
||||
name: string
|
||||
streetAddress: string
|
||||
area: string
|
||||
city: string
|
||||
}
|
||||
|
||||
const propData: propsType[] = [
|
||||
{
|
||||
typeOfAddress: 'Home',
|
||||
isDefaultAddress: true,
|
||||
name: 'Violet Mendoza',
|
||||
streetAddress: '23 Shatinon Mekalan',
|
||||
area: 'Melbourne, VIC 3000,',
|
||||
city: 'London'
|
||||
},
|
||||
{
|
||||
typeOfAddress: 'Office',
|
||||
isDefaultAddress: false,
|
||||
name: 'Archie Mendoza',
|
||||
streetAddress: '45 Roker Terrace',
|
||||
area: 'Latheronwheel',
|
||||
city: 'London'
|
||||
},
|
||||
{
|
||||
typeOfAddress: 'Family',
|
||||
isDefaultAddress: false,
|
||||
name: 'George Mendoza',
|
||||
streetAddress: '512 Water Plant',
|
||||
area: 'Melbourne, VIC 3000',
|
||||
city: 'London'
|
||||
}
|
||||
]
|
||||
|
||||
// Vars
|
||||
const data = {
|
||||
firstName: 'Violet',
|
||||
lastName: 'Mendoza',
|
||||
email: 'sbaser0@boston.com',
|
||||
country: 'UK',
|
||||
address1: '23 Shatinon Mekalan',
|
||||
address2: 'Melbourne, VIC 3000',
|
||||
landmark: 'Near Water Plant',
|
||||
city: 'London',
|
||||
state: 'Capholim',
|
||||
zipCode: '403114',
|
||||
taxId: 'TAX-875623',
|
||||
vatNumber: 'SDF754K77',
|
||||
contact: '+1 (234) 464-0600'
|
||||
}
|
||||
|
||||
const CustomerAddress = (props: propsType) => {
|
||||
// Props
|
||||
const { typeOfAddress, isDefaultAddress, name, streetAddress, area, city } = props
|
||||
|
||||
// States
|
||||
const [expanded, setExpanded] = useState(isDefaultAddress ? true : false)
|
||||
|
||||
// Vars
|
||||
const iconButtonProps: IconButtonProps = {
|
||||
children: <i className='tabler-edit' />,
|
||||
className: 'text-textSecondary'
|
||||
}
|
||||
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-wrap justify-between items-center mlb-3 gap-y-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<IconButton
|
||||
size='large'
|
||||
sx={{
|
||||
'& i': {
|
||||
transition: 'transform 0.3s',
|
||||
transform: expanded ? 'rotate(0deg)' : theme.direction === 'ltr' ? 'rotate(-90deg)' : 'rotate(90deg)'
|
||||
}
|
||||
}}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<i className='tabler-chevron-down text-textPrimary' />
|
||||
</IconButton>
|
||||
<div className='flex flex-col items-start gap-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
{typeOfAddress}
|
||||
</Typography>
|
||||
{isDefaultAddress && <Chip variant='tonal' color='success' label='Default Address' size='small' />}
|
||||
</div>
|
||||
<Typography>{streetAddress}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mis-10'>
|
||||
<OpenDialogOnElementClick
|
||||
element={IconButton}
|
||||
elementProps={iconButtonProps}
|
||||
dialog={AddNewAddress}
|
||||
dialogProps={{ data }}
|
||||
/>
|
||||
<IconButton>
|
||||
<i className='tabler-trash text-textSecondary' />
|
||||
</IconButton>
|
||||
<OptionMenu
|
||||
iconClassName='text-textSecondary'
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
options={['Set as Default Address']}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Collapse in={expanded} timeout={300}>
|
||||
<div className='flex flex-col gap-1 pb-3 pis-14'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
{name}
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography>{streetAddress}</Typography>
|
||||
<Typography>{area}</Typography>
|
||||
<Typography>{city}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const AddressBook = () => {
|
||||
// Vars
|
||||
const buttonProps: ButtonProps = {
|
||||
variant: 'tonal',
|
||||
children: 'Add New Address',
|
||||
size: 'small'
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title='Address Book'
|
||||
action={<OpenDialogOnElementClick element={Button} elementProps={buttonProps} dialog={AddNewAddress} />}
|
||||
/>
|
||||
<CardContent>
|
||||
{propData.map((address, index) => (
|
||||
<div key={index}>
|
||||
<CustomerAddress {...address} />
|
||||
{index !== propData.length - 1 && <Divider />}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressBook
|
||||
-242
@@ -1,242 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Collapse from '@mui/material/Collapse'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Button from '@mui/material/Button'
|
||||
import type { ButtonProps } from '@mui/material/Button'
|
||||
import type { IconButtonProps } from '@mui/material/IconButton'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
import AddNewCard from '@components/dialogs/billing-card'
|
||||
import OpenDialogOnElementClick from '@components/dialogs/OpenDialogOnElementClick'
|
||||
|
||||
type dataType = {
|
||||
typeOfCard: string
|
||||
isDefaultCard: boolean
|
||||
expiryDate: string
|
||||
}
|
||||
|
||||
const data: dataType[] = [
|
||||
{
|
||||
typeOfCard: 'Mastercard',
|
||||
isDefaultCard: true,
|
||||
expiryDate: 'Apr 2028'
|
||||
},
|
||||
{
|
||||
typeOfCard: 'American Express',
|
||||
isDefaultCard: false,
|
||||
expiryDate: 'Jan 2025'
|
||||
},
|
||||
{
|
||||
typeOfCard: 'Visa',
|
||||
isDefaultCard: false,
|
||||
expiryDate: 'Nov 2030'
|
||||
}
|
||||
]
|
||||
|
||||
// Vars
|
||||
const editCardData = {
|
||||
cardNumber: '**** **** **** 4487',
|
||||
name: 'Violet Mendoza ',
|
||||
expiryDate: '04/2028',
|
||||
cardCvv: '233'
|
||||
}
|
||||
|
||||
const CustomerAddress = (props: dataType) => {
|
||||
// Props
|
||||
const { typeOfCard, isDefaultCard, expiryDate } = props
|
||||
|
||||
// States
|
||||
const [expanded, setExpanded] = useState(isDefaultCard ? true : false)
|
||||
|
||||
// Vars
|
||||
const iconButtonProps: IconButtonProps = {
|
||||
children: <i className='tabler-edit' />,
|
||||
className: 'text-textSecondary'
|
||||
}
|
||||
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
|
||||
const mastercard = '/images/apps/ecommerce/mastercard.png'
|
||||
const americanExpress = '/images/apps/ecommerce/american-express.png'
|
||||
const visa = '/images/apps/ecommerce/visa.png'
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-wrap justify-between items-center mlb-3 gap-y-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<IconButton
|
||||
size='large'
|
||||
sx={{
|
||||
'& i': {
|
||||
transition: 'transform 0.3s',
|
||||
transform: expanded ? 'rotate(0deg)' : theme.direction === 'ltr' ? 'rotate(-90deg)' : 'rotate(90deg)'
|
||||
}
|
||||
}}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<i className='tabler-chevron-down text-textPrimary' />
|
||||
</IconButton>
|
||||
<div className='flex items-center gap-4'>
|
||||
<div className='flex justify-center items-center bg-[#F6F8FA] rounded-sm is-[50px] bs-[30px]'>
|
||||
<img
|
||||
src={
|
||||
typeOfCard === 'Mastercard' ? mastercard : typeOfCard === 'American Express' ? americanExpress : visa
|
||||
}
|
||||
alt={typeOfCard}
|
||||
height={typeOfCard === 'Mastercard' ? 19 : typeOfCard === 'American Express' ? 16 : 12}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col items-start gap-1'>
|
||||
<div className='flex flex-wrap items-center gap-x-2 gap-y-1'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
{typeOfCard}
|
||||
</Typography>
|
||||
{isDefaultCard && <Chip variant='tonal' color='success' label='Default Card' size='small' />}
|
||||
</div>
|
||||
<Typography>Expires {expiryDate}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mis-10'>
|
||||
<OpenDialogOnElementClick
|
||||
element={IconButton}
|
||||
elementProps={iconButtonProps}
|
||||
dialog={AddNewCard}
|
||||
dialogProps={{ data: editCardData }}
|
||||
/>
|
||||
<IconButton>
|
||||
<i className='tabler-trash text-textSecondary' />
|
||||
</IconButton>
|
||||
<OptionMenu
|
||||
iconClassName='text-textSecondary'
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
options={['Set as Default Card']}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Collapse in={expanded} timeout={300}>
|
||||
<Grid container spacing={6} className='pbe-3 pis-12'>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 4 }}>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography variant='body2'>Name</Typography>
|
||||
<Typography variant='body2'>Number</Typography>
|
||||
<Typography variant='body2'>Expires</Typography>
|
||||
<Typography variant='body2'>Type</Typography>
|
||||
<Typography variant='body2'>Issuer</Typography>
|
||||
<Typography variant='body2'>ID</Typography>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 8 }}>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography variant='body2' color='text.primary' className='font-medium'>
|
||||
Violet Mendoza
|
||||
</Typography>
|
||||
<Typography variant='body2' color='text.primary' className='font-medium'>
|
||||
**** 4487
|
||||
</Typography>
|
||||
<Typography variant='body2' color='text.primary' className='font-medium'>
|
||||
08/2028
|
||||
</Typography>
|
||||
<Typography variant='body2' color='text.primary' className='font-medium'>
|
||||
{typeOfCard}
|
||||
</Typography>
|
||||
<Typography variant='body2' color='text.primary' className='font-medium'>
|
||||
VICBANK
|
||||
</Typography>
|
||||
<Typography variant='body2' color='text.primary' className='font-medium'>
|
||||
DH73DJ8
|
||||
</Typography>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Grid container>
|
||||
<Grid size={{ xs: 4 }}>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography variant='body2'>Billing</Typography>
|
||||
<Typography variant='body2'>Number</Typography>
|
||||
<Typography variant='body2'>Email</Typography>
|
||||
<Typography variant='body2'>Origin</Typography>
|
||||
<Typography variant='body2'>CVC</Typography>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 8 }}>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography variant='body2' color='text.primary' className='font-medium'>
|
||||
USA
|
||||
</Typography>
|
||||
<Typography variant='body2' color='text.primary' className='font-medium'>
|
||||
+7634 983 637
|
||||
</Typography>
|
||||
<Typography variant='body2' color='text.primary' className='font-medium'>
|
||||
vafgot@vultukir.org
|
||||
</Typography>
|
||||
<div className='flex gap-2'>
|
||||
<Typography variant='body2' color='text.primary' className='font-medium'>
|
||||
United States
|
||||
</Typography>
|
||||
<img src='/images/cards/us.png' height={20} />
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
<Typography variant='body2' color='text.primary' className='font-medium'>
|
||||
Passed
|
||||
</Typography>
|
||||
<CustomAvatar skin='light' size={20} color='success'>
|
||||
<i className='tabler-check text-xs' />
|
||||
</CustomAvatar>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Collapse>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const PaymentMethod = () => {
|
||||
// Vars
|
||||
const buttonProps: ButtonProps = {
|
||||
variant: 'tonal',
|
||||
children: 'New Payment Methods',
|
||||
size: 'small'
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title='Payment Methods'
|
||||
action={<OpenDialogOnElementClick element={Button} elementProps={buttonProps} dialog={AddNewCard} />}
|
||||
className='flex-wrap gap-4'
|
||||
/>
|
||||
<CardContent>
|
||||
{data.map((address, index) => (
|
||||
<div key={index}>
|
||||
<CustomerAddress {...address} />
|
||||
{index !== data.length - 1 && <Divider />}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaymentMethod
|
||||
@@ -1,23 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import AddressBook from './AddressBookCard'
|
||||
import PaymentMethod from './PaymentMethodCard'
|
||||
|
||||
const AddressBilling = () => {
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<AddressBook />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<PaymentMethod />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressBilling
|
||||
@@ -1,57 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { SyntheticEvent, ReactElement } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Tab from '@mui/material/Tab'
|
||||
import TabContext from '@mui/lab/TabContext'
|
||||
import TabPanel from '@mui/lab/TabPanel'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import CustomTabList from '@core/components/mui/TabList'
|
||||
|
||||
const CustomerRight = ({ tabContentList }: { tabContentList: { [key: string]: ReactElement } }) => {
|
||||
// States
|
||||
const [activeTab, setActiveTab] = useState('overview')
|
||||
|
||||
const handleChange = (event: SyntheticEvent, value: string) => {
|
||||
setActiveTab(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabContext value={activeTab}>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CustomTabList onChange={handleChange} variant='scrollable' pill='true'>
|
||||
<Tab icon={<i className='tabler-user' />} value='overview' label='Overview' iconPosition='start' />
|
||||
<Tab icon={<i className='tabler-lock' />} value='security' label='Security' iconPosition='start' />
|
||||
<Tab
|
||||
icon={<i className='tabler-map-pin' />}
|
||||
value='addressBilling'
|
||||
label='Address & Billing'
|
||||
iconPosition='start'
|
||||
/>
|
||||
<Tab
|
||||
icon={<i className='tabler-bell' />}
|
||||
value='notifications'
|
||||
label='Notifications'
|
||||
iconPosition='start'
|
||||
/>
|
||||
</CustomTabList>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TabPanel value={activeTab} className='p-0'>
|
||||
{tabContentList[activeTab]}
|
||||
</TabPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabContext>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerRight
|
||||
@@ -1,93 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardActions from '@mui/material/CardActions'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import Button from '@mui/material/Button'
|
||||
|
||||
// Style Imports
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
|
||||
type TableDataType = {
|
||||
type: string
|
||||
app: boolean
|
||||
email: boolean
|
||||
browser: boolean
|
||||
}
|
||||
|
||||
// Vars
|
||||
const tableData: TableDataType[] = [
|
||||
{
|
||||
app: false,
|
||||
email: true,
|
||||
browser: false,
|
||||
type: 'New for you'
|
||||
},
|
||||
{
|
||||
app: true,
|
||||
email: false,
|
||||
browser: true,
|
||||
type: 'Account activity'
|
||||
},
|
||||
{
|
||||
app: true,
|
||||
email: true,
|
||||
browser: true,
|
||||
type: 'A new browser used to sign in'
|
||||
},
|
||||
{
|
||||
app: false,
|
||||
email: false,
|
||||
browser: true,
|
||||
type: 'A new device is linked'
|
||||
}
|
||||
]
|
||||
|
||||
const Notification = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Notifications' subheader='You will receive notification for the below selected items' />
|
||||
<div className='overflow-x-auto'>
|
||||
<table className={tableStyles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Email</th>
|
||||
<th>Browser</th>
|
||||
<th>App</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className='border-be'>
|
||||
{tableData.map((data, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<Typography color='text.primary'>{data.type}</Typography>
|
||||
</td>
|
||||
<td>
|
||||
<Checkbox defaultChecked={data.email} />
|
||||
</td>
|
||||
<td>
|
||||
<Checkbox defaultChecked={data.browser} />
|
||||
</td>
|
||||
<td>
|
||||
<Checkbox defaultChecked={data.app} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<CardActions className='flex items-center'>
|
||||
<Button variant='contained' type='submit'>
|
||||
Save Changes
|
||||
</Button>
|
||||
<Button variant='tonal' color='secondary' type='reset'>
|
||||
Discard
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default Notification
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Types Imports
|
||||
import type { CardStatsCustomerStatsProps } from '@/types/pages/widgetTypes'
|
||||
|
||||
// Component Imports
|
||||
import CustomerStats from '@components/card-statistics/CustomerStats'
|
||||
|
||||
const CustomerStatisticsCard = ({ customerStatData }: { customerStatData?: CardStatsCustomerStatsProps[] }) => {
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
{customerStatData?.map((item, index) => (
|
||||
<Grid size={{ xs: 12, md: 6 }} key={index}>
|
||||
<CustomerStats {...item} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerStatisticsCard
|
||||
-312
@@ -1,312 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import type { TextFieldProps } from '@mui/material/TextField'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Third-party Imports
|
||||
import type { RankingInfo } from '@tanstack/match-sorter-utils'
|
||||
import { rankItem } from '@tanstack/match-sorter-utils'
|
||||
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedMinMaxValues,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable
|
||||
} from '@tanstack/react-table'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { OrderType } from '@/types/apps/ecommerceTypes'
|
||||
import type { Locale } from '@configs/i18n'
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
// Util Imports
|
||||
import { getLocalizedUrl } from '@/utils/i18n'
|
||||
|
||||
// Style Imports
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface FilterFns {
|
||||
fuzzy: FilterFn<unknown>
|
||||
}
|
||||
interface FilterMeta {
|
||||
itemRank: RankingInfo
|
||||
}
|
||||
}
|
||||
|
||||
type PayementStatusType = {
|
||||
text: string
|
||||
color: ThemeColor
|
||||
}
|
||||
|
||||
type StatusChipColorType = {
|
||||
color: ThemeColor
|
||||
}
|
||||
|
||||
export const paymentStatus: { [key: number]: PayementStatusType } = {
|
||||
1: { text: 'Paid', color: 'success' },
|
||||
2: { text: 'Pending', color: 'warning' },
|
||||
3: { text: 'Cancelled', color: 'secondary' },
|
||||
4: { text: 'Failed', color: 'error' }
|
||||
}
|
||||
|
||||
export const statusChipColor: { [key: string]: StatusChipColorType } = {
|
||||
Delivered: { color: 'success' },
|
||||
'Out for Delivery': { color: 'primary' },
|
||||
'Ready to Pickup': { color: 'info' },
|
||||
Dispatched: { color: 'warning' }
|
||||
}
|
||||
|
||||
type ECommerceOrderTypeWithAction = OrderType & {
|
||||
action?: string
|
||||
}
|
||||
|
||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value)
|
||||
|
||||
// Store the itemRank info
|
||||
addMeta({
|
||||
itemRank
|
||||
})
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed
|
||||
}
|
||||
|
||||
const DebouncedInput = ({
|
||||
value: initialValue,
|
||||
onChange,
|
||||
debounce = 500,
|
||||
...props
|
||||
}: {
|
||||
value: string | number
|
||||
onChange: (value: string | number) => void
|
||||
debounce?: number
|
||||
} & Omit<TextFieldProps, 'onChange'>) => {
|
||||
// States
|
||||
const [value, setValue] = useState(initialValue)
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue)
|
||||
}, [initialValue])
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
onChange(value)
|
||||
}, debounce)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value])
|
||||
|
||||
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
|
||||
}
|
||||
|
||||
// Column Definitions
|
||||
const columnHelper = createColumnHelper<ECommerceOrderTypeWithAction>()
|
||||
|
||||
const OrderListTable = ({ orderData }: { orderData?: OrderType[] }) => {
|
||||
// States
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
const [data, setData] = useState(...[orderData])
|
||||
const [globalFilter, setGlobalFilter] = useState('')
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
const columns = useMemo<ColumnDef<ECommerceOrderTypeWithAction, any>[]>(
|
||||
() => [
|
||||
columnHelper.accessor('order', {
|
||||
header: 'order',
|
||||
cell: ({ row }) => (
|
||||
<Typography
|
||||
component={Link}
|
||||
href={getLocalizedUrl(`/apps/ecommerce/orders/details/${row.original.order}`, locale as Locale)}
|
||||
color='primary.main'
|
||||
>{`#${row.original.order}`}</Typography>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('date', {
|
||||
header: 'Date',
|
||||
cell: ({ row }) => <Typography>{`${new Date(row.original.date).toDateString()}`}</Typography>
|
||||
}),
|
||||
columnHelper.accessor('status', {
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<Chip
|
||||
label={row.original.status}
|
||||
color={statusChipColor[row.original.status].color}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
/>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('spent', {
|
||||
header: 'Spent',
|
||||
cell: ({ row }) => <Typography>${row.original.spent}</Typography>
|
||||
}),
|
||||
columnHelper.accessor('action', {
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center'>
|
||||
<OptionMenu
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
iconClassName='text-textSecondary text-[22px]'
|
||||
options={[
|
||||
{
|
||||
text: 'View',
|
||||
icon: 'tabler-eye',
|
||||
href: getLocalizedUrl(`/apps/ecommerce/orders/details/${row.original.order}`, locale as Locale),
|
||||
linkProps: { className: 'flex items-center is-full plb-1.5 pli-4' }
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
icon: 'tabler-trash text-[22px]',
|
||||
menuItemProps: {
|
||||
onClick: () => setData(data?.filter(order => order.id !== row.original.id)),
|
||||
className: 'flex items-center'
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false
|
||||
})
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[data]
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data: data as OrderType[],
|
||||
columns,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter
|
||||
},
|
||||
state: {
|
||||
rowSelection,
|
||||
globalFilter
|
||||
},
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: 6
|
||||
}
|
||||
},
|
||||
enableRowSelection: true, //enable row selection for all rows
|
||||
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
|
||||
globalFilterFn: fuzzyFilter,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
getFacetedMinMaxValues: getFacetedMinMaxValues()
|
||||
})
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className='flex justify-between flex-col items-start sm:flex-row sm:items-center gap-y-4'>
|
||||
<Typography variant='h5'>Orders Placed</Typography>
|
||||
<DebouncedInput
|
||||
value={globalFilter ?? ''}
|
||||
onChange={value => setGlobalFilter(String(value))}
|
||||
placeholder='Search Order'
|
||||
className='max-sm:is-full'
|
||||
/>
|
||||
</CardContent>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className={tableStyles.table}>
|
||||
<thead>
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<th key={header.id}>
|
||||
{header.isPlaceholder ? null : (
|
||||
<>
|
||||
<div
|
||||
className={classnames({
|
||||
'flex items-center': header.column.getIsSorted(),
|
||||
'cursor-pointer select-none': header.column.getCanSort()
|
||||
})}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{{
|
||||
asc: <i className='tabler-chevron-up text-xl' />,
|
||||
desc: <i className='tabler-chevron-down text-xl' />
|
||||
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
{table.getFilteredRowModel().rows.length === 0 ? (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
||||
No data available
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
) : (
|
||||
<tbody>
|
||||
{table
|
||||
.getRowModel()
|
||||
.rows.slice(0, table.getState().pagination.pageSize)
|
||||
.map(row => {
|
||||
return (
|
||||
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
{/* <TablePagination
|
||||
component={() => <TablePaginationComponent table={table} />}
|
||||
count={table.getFilteredRowModel().rows.length}
|
||||
rowsPerPage={table.getState().pagination.pageSize}
|
||||
page={table.getState().pagination.pageIndex}
|
||||
onPageChange={(_, page) => {
|
||||
table.setPageIndex(page)
|
||||
}}
|
||||
/> */}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrderListTable
|
||||
@@ -1,64 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import CustomerStatisticsCard from './CustomerStatisticsCard'
|
||||
import OrderListTable from './OrderListTable'
|
||||
|
||||
// Data Imports
|
||||
import { getStatisticsData, getEcommerceData } from '@/app/server/actions'
|
||||
|
||||
/**
|
||||
* ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the
|
||||
* ! `.env` file found at root of your project and also update the API endpoints like `/pages/widget-examples` in below example.
|
||||
* ! Also, remove the above server action import and the action itself from the `src/app/server/actions.ts` file to clean up unused code
|
||||
* ! because we've used the server action for getting our static data.
|
||||
*/
|
||||
|
||||
/* const getStatisticsData = async () => {
|
||||
// Vars
|
||||
const res = await fetch(`${process.env.API_URL}/pages/widget-examples`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch statistics data')
|
||||
}
|
||||
|
||||
return res.json()
|
||||
} */
|
||||
|
||||
/**
|
||||
* ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the
|
||||
* ! `.env` file found at root of your project and also update the API endpoints like `/apps/ecommerce` in below example.
|
||||
* ! Also, remove the above server action import and the action itself from the `src/app/server/actions.ts` file to clean up unused code
|
||||
* ! because we've used the server action for getting our static data.
|
||||
*/
|
||||
|
||||
/* const getEcommerceData = async () => {
|
||||
// Vars
|
||||
const res = await fetch(`${process.env.API_URL}/apps/ecommerce`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch ecommerce data')
|
||||
}
|
||||
|
||||
return res.json()
|
||||
} */
|
||||
|
||||
const Overview = async () => {
|
||||
// Vars
|
||||
const data = await getStatisticsData()
|
||||
const tableData = await getEcommerceData()
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CustomerStatisticsCard customerStatData={data?.customerStats} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<OrderListTable orderData={tableData?.orderData} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default Overview
|
||||
@@ -1,95 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import InputAdornment from '@mui/material/InputAdornment'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import AlertTitle from '@mui/material/AlertTitle'
|
||||
import Button from '@mui/material/Button'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
const ChangePassword = () => {
|
||||
// States
|
||||
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
||||
const [isConfirmPasswordShown, setIsConfirmPasswordShown] = useState(false)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Change Password' />
|
||||
<CardContent className='flex flex-col gap-4'>
|
||||
<Alert icon={false} severity='warning' onClose={() => {}}>
|
||||
<AlertTitle>Ensure that these requirements are met</AlertTitle>
|
||||
Minimum 8 characters long, uppercase & symbol
|
||||
</Alert>
|
||||
<form>
|
||||
<Grid container spacing={4}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Password'
|
||||
type={isPasswordShown ? 'text' : 'password'}
|
||||
placeholder='············'
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
<IconButton
|
||||
edge='end'
|
||||
onClick={() => setIsPasswordShown(!isPasswordShown)}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
<i className={isPasswordShown ? 'tabler-eye-off' : 'tabler-eye'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Confirm Password'
|
||||
type={isConfirmPasswordShown ? 'text' : 'password'}
|
||||
placeholder='············'
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
<IconButton
|
||||
edge='end'
|
||||
onClick={() => setIsConfirmPasswordShown(!isConfirmPasswordShown)}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
<i className={isConfirmPasswordShown ? 'tabler-eye-off' : 'tabler-eye'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Button variant='contained'>Change Password</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangePassword
|
||||
@@ -1,93 +0,0 @@
|
||||
// React Imports
|
||||
import type { ReactElement } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Style Imports
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
|
||||
type DataType = {
|
||||
device: string
|
||||
browser: string
|
||||
location: string
|
||||
recentActivity: string
|
||||
browserIcon: ReactElement
|
||||
}
|
||||
|
||||
// Vars
|
||||
const recentDeviceData: DataType[] = [
|
||||
{
|
||||
device: 'Dell XPS 15',
|
||||
location: 'United States',
|
||||
browser: 'Chrome on Windows',
|
||||
recentActivity: '10, Jan 2020 20:07',
|
||||
browserIcon: <i className='tabler-brand-windows text-[22px] text-info' />
|
||||
},
|
||||
{
|
||||
location: 'Ghana',
|
||||
device: 'Google Pixel 3a',
|
||||
browser: 'Chrome on Android',
|
||||
recentActivity: '11, Jan 2020 10:16',
|
||||
browserIcon: <i className='tabler-brand-android text-[22px] text-success' />
|
||||
},
|
||||
{
|
||||
location: 'Mayotte',
|
||||
device: 'Apple iMac',
|
||||
browser: 'Chrome on MacOS',
|
||||
recentActivity: '11, Jan 2020 12:10',
|
||||
browserIcon: <i className='tabler-brand-apple text-[22px] text-secondary' />
|
||||
},
|
||||
{
|
||||
location: 'Mauritania',
|
||||
device: 'Apple iPhone XR',
|
||||
browser: 'Chrome on iPhone',
|
||||
recentActivity: '12, Jan 2020 8:29',
|
||||
browserIcon: <i className='tabler-device-mobile text-[22px] text-error' />
|
||||
}
|
||||
]
|
||||
|
||||
const RecentDevice = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Recent Devices' />
|
||||
<div className='overflow-x-auto'>
|
||||
<table className={tableStyles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Browser</th>
|
||||
<th>Device</th>
|
||||
<th>Location</th>
|
||||
<th>Recent Activities</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recentDeviceData.map((device, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<div className='flex items-center gap-4'>
|
||||
{device.browserIcon}
|
||||
<Typography color='text.primary'>{device.browser}</Typography>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<Typography>{device.device}</Typography>
|
||||
</td>
|
||||
<td>
|
||||
<Typography>{device.location}</Typography>
|
||||
</td>
|
||||
<td>
|
||||
<Typography>{device.recentActivity}</Typography>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecentDevice
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import InputLabel from '@mui/material/InputLabel'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Component Imports
|
||||
import Link from '@components/Link'
|
||||
import CustomIconButton from '@core/components/mui/IconButton'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
const TwoStepVerification = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Two-step verification' subheader='Keep your account secure with authentication step.' />
|
||||
<CardContent>
|
||||
<InputLabel htmlFor='sms' className='font-medium text-textPrimary mbe-1'>
|
||||
SMS
|
||||
</InputLabel>
|
||||
<div className='flex items-center gap-4 mbe-4'>
|
||||
<CustomTextField id='sms' placeholder='+1(968) 819-2547' fullWidth />
|
||||
<div className='flex'>
|
||||
<CustomIconButton>
|
||||
<i className='tabler-edit text-textPrimary' />
|
||||
</CustomIconButton>
|
||||
<CustomIconButton>
|
||||
<i className='tabler-user-plus text-textPrimary' />
|
||||
</CustomIconButton>
|
||||
</div>
|
||||
</div>
|
||||
<Typography>
|
||||
Two-factor authentication adds an additional layer of security to your account by requiring more than just a
|
||||
password to log in.{' '}
|
||||
<Typography component={Link} color='primary.main'>
|
||||
Learn more.
|
||||
</Typography>
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default TwoStepVerification
|
||||
@@ -1,25 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import ChangePassword from './ChangePassword'
|
||||
import TwoStepVerification from './TwoStepVerification'
|
||||
import RecentDevice from './RecentDevice'
|
||||
|
||||
const SecurityTab = () => {
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<ChangePassword />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TwoStepVerification />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<RecentDevice />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default SecurityTab
|
||||
@@ -1,50 +0,0 @@
|
||||
// React Imports
|
||||
import type { ReactElement } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Type Imports
|
||||
import type { Customer } from '@/types/apps/ecommerceTypes'
|
||||
|
||||
// Component Imports
|
||||
import CustomerDetailsHeader from './CustomerDetailsHeader'
|
||||
import CustomerLeftOverview from './customer-left-overview'
|
||||
import CustomerRight from './customer-right'
|
||||
|
||||
const OverViewTab = dynamic(() => import('@views/apps/ecommerce/customers/details/customer-right/overview'))
|
||||
const SecurityTab = dynamic(() => import('@views/apps/ecommerce/customers/details/customer-right/security'))
|
||||
const NotificationsTab = dynamic(() => import('@views/apps/ecommerce/customers/details/customer-right/notification'))
|
||||
|
||||
const AddressBillingTab = dynamic(
|
||||
() => import('@views/apps/ecommerce/customers/details/customer-right/address-billing')
|
||||
)
|
||||
|
||||
// Vars
|
||||
const tabContentList = (): { [key: string]: ReactElement } => ({
|
||||
overview: <OverViewTab />,
|
||||
security: <SecurityTab />,
|
||||
addressBilling: <AddressBillingTab />,
|
||||
notifications: <NotificationsTab />
|
||||
})
|
||||
|
||||
const CustomerDetails = ({ customerData, customerId }: { customerData?: Customer; customerId: string }) => {
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CustomerDetailsHeader customerId={customerId} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<CustomerLeftOverview customerData={customerData} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<CustomerRight tabContentList={tabContentList()} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerDetails
|
||||
@@ -1,263 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import Button from '@mui/material/Button'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import InputBase from '@mui/material/InputBase'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import { StarterKit } from '@tiptap/starter-kit'
|
||||
import { Underline } from '@tiptap/extension-underline'
|
||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||
import { TextAlign } from '@tiptap/extension-text-align'
|
||||
import type { Editor } from '@tiptap/core'
|
||||
|
||||
// Component Imports
|
||||
import CustomIconButton from '@core/components/mui/IconButton'
|
||||
|
||||
// Hook Imports
|
||||
import { useSettings } from '@core/hooks/useSettings'
|
||||
|
||||
// Style Imports
|
||||
import '@/libs/styles/tiptapEditor.css'
|
||||
|
||||
type Props = {
|
||||
openCompose: boolean
|
||||
setOpenCompose: (value: boolean) => void
|
||||
isBelowSmScreen: boolean
|
||||
isBelowMdScreen: boolean
|
||||
}
|
||||
|
||||
const EditorToolbar = ({ editor }: { editor: Editor | null }) => {
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap gap-x-3 gap-y-1 plb-2 pli-4 border-bs'>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive('bold') && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
>
|
||||
<i className={classnames('tabler-bold', { 'text-textSecondary': !editor.isActive('bold') })} />
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive('underline') && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
>
|
||||
<i className={classnames('tabler-underline', { 'text-textSecondary': !editor.isActive('underline') })} />
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive('italic') && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
>
|
||||
<i className={classnames('tabler-italic', { 'text-textSecondary': !editor.isActive('italic') })} />
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive('strike') && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
>
|
||||
<i className={classnames('tabler-strikethrough', { 'text-textSecondary': !editor.isActive('strike') })} />
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive({ textAlign: 'left' }) && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||
>
|
||||
<i
|
||||
className={classnames('tabler-align-left', { 'text-textSecondary': !editor.isActive({ textAlign: 'left' }) })}
|
||||
/>
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive({ textAlign: 'center' }) && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||
>
|
||||
<i
|
||||
className={classnames('tabler-align-center', {
|
||||
'text-textSecondary': !editor.isActive({ textAlign: 'center' })
|
||||
})}
|
||||
/>
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive({ textAlign: 'right' }) && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||
>
|
||||
<i
|
||||
className={classnames('tabler-align-right', {
|
||||
'text-textSecondary': !editor.isActive({ textAlign: 'right' })
|
||||
})}
|
||||
/>
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive({ textAlign: 'justify' }) && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
||||
>
|
||||
<i
|
||||
className={classnames('tabler-align-justified', {
|
||||
'text-textSecondary': !editor.isActive({ textAlign: 'justify' })
|
||||
})}
|
||||
/>
|
||||
</CustomIconButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ComposeMail = (props: Props) => {
|
||||
// Props
|
||||
const { openCompose, setOpenCompose, isBelowSmScreen, isBelowMdScreen } = props
|
||||
|
||||
// States
|
||||
const [visibility, setVisibility] = useState({ cc: false, bcc: false })
|
||||
|
||||
// Hooks
|
||||
const { settings } = useSettings()
|
||||
|
||||
const toggleVisibility = (value: 'cc' | 'bcc') => {
|
||||
setVisibility(prev => ({ ...prev, [value]: !prev[value] }))
|
||||
}
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Placeholder.configure({
|
||||
placeholder: 'Message'
|
||||
}),
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph']
|
||||
}),
|
||||
Underline
|
||||
],
|
||||
immediatelyRender: false
|
||||
})
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
anchor='bottom'
|
||||
variant='persistent'
|
||||
hideBackdrop
|
||||
open={openCompose}
|
||||
onClose={() => setOpenCompose(false)}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: isBelowMdScreen ? 'calc(100% - 2 * 1.5rem)' : '100%',
|
||||
maxWidth: 600,
|
||||
position: 'absolute',
|
||||
height: 'auto',
|
||||
insetInlineStart: 'auto',
|
||||
insetInlineEnd: '1.5rem',
|
||||
insetBlockEnd: '1.5rem',
|
||||
borderRadius: 'var(--mui-shape-borderRadius)',
|
||||
borderTop: 0,
|
||||
boxShadow: settings.skin === 'bordered' ? 'none' : 'var(--mui-customShadows-xl)',
|
||||
border: settings.skin === 'bordered' ? '1px solid var(--mui-palette-divider)' : undefined,
|
||||
zIndex: 12
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-between plb-3.5 pli-6 bg-actionHover'>
|
||||
<Typography variant='h5' color='text.secondary'>
|
||||
Compose Mail
|
||||
</Typography>
|
||||
<div className='flex gap-2'>
|
||||
<IconButton size='small' onClick={() => setOpenCompose(false)}>
|
||||
<i className='tabler-minus text-textSecondary' />
|
||||
</IconButton>
|
||||
<IconButton size='small' onClick={() => setOpenCompose(false)}>
|
||||
<i className='tabler-x text-textSecondary' />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 pli-6 plb-1'>
|
||||
<Typography className='font-medium' color='text.disabled'>
|
||||
To:
|
||||
</Typography>
|
||||
<InputBase fullWidth />
|
||||
<div className='text-textSecondary'>
|
||||
<span className='cursor-pointer' onClick={() => toggleVisibility('cc')}>
|
||||
Cc
|
||||
</span>
|
||||
<span className='mli-1'>|</span>
|
||||
<span className='cursor-pointer' onClick={() => toggleVisibility('bcc')}>
|
||||
Bcc
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{visibility.cc && (
|
||||
<InputBase
|
||||
className='plb-1 pli-6 border-bs'
|
||||
startAdornment={
|
||||
<Typography className='font-medium mie-2' color='text.disabled'>
|
||||
Cc:
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{visibility.bcc && (
|
||||
<InputBase
|
||||
className='plb-1 pli-6 border-bs'
|
||||
startAdornment={
|
||||
<Typography className='font-medium mie-2' color='text.disabled'>
|
||||
Bcc:
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<InputBase
|
||||
className='plb-1 pli-6 border-bs'
|
||||
startAdornment={
|
||||
<Typography className='font-medium mie-2' color='text.disabled'>
|
||||
Subject:
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<EditorToolbar editor={editor} />
|
||||
<EditorContent editor={editor} className='bs-[105px] overflow-y-auto flex border-bs' />
|
||||
<div className='plb-4 pli-5 flex justify-between items-center gap-4'>
|
||||
<div className='flex items-center gap-4 max-sm:gap-3'>
|
||||
{isBelowSmScreen ? (
|
||||
<CustomIconButton color='primary' variant='contained'>
|
||||
<i className='tabler-send' />
|
||||
</CustomIconButton>
|
||||
) : (
|
||||
<Button variant='contained' endIcon={<i className='tabler-send' />} onClick={() => setOpenCompose(false)}>
|
||||
Send
|
||||
</Button>
|
||||
)}
|
||||
<IconButton size='small'>
|
||||
<i className='tabler-paperclip text-textSecondary' />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
<IconButton size='small'>
|
||||
<i className='tabler-dots-vertical text-textSecondary' />
|
||||
</IconButton>
|
||||
<IconButton size='small' onClick={() => setOpenCompose(false)}>
|
||||
<i className='tabler-trash text-textSecondary' />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default ComposeMail
|
||||
@@ -1,95 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { Email } from '@/types/apps/emailTypes'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
// Styles Imports
|
||||
import styles from './styles.module.css'
|
||||
|
||||
const CardHeaderAction = ({ data, isReplies }: { data: Email; isReplies: boolean }) => {
|
||||
return (
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography color='text.disabled'>
|
||||
{new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
}).format(new Date(data.time))}
|
||||
</Typography>
|
||||
<div className='flex items-center gap-1'>
|
||||
{data.attachments.length ? (
|
||||
<IconButton>
|
||||
<i className='tabler-paperclip text-textSecondary' />
|
||||
</IconButton>
|
||||
) : null}
|
||||
{isReplies ? (
|
||||
<OptionMenu
|
||||
iconClassName='text-textSecondary'
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
options={[
|
||||
{ text: 'Reply', icon: 'tabler-arrow-back-up' },
|
||||
{ text: 'Forward', icon: 'tabler-arrow-forward-up' }
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<IconButton>
|
||||
<i className='tabler-dots-vertical text-textSecondary' />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MailCard = ({ data, isReplies }: { data: Email; isReplies: boolean }) => {
|
||||
return (
|
||||
<Card className='border'>
|
||||
<CardContent className='flex is-full gap-4'>
|
||||
<CustomAvatar src={data.from.avatar} size={38} alt={data.from.name} />
|
||||
<div className='flex items-center justify-between flex-wrap grow gap-x-4 gap-y-2'>
|
||||
<div className='flex flex-col'>
|
||||
<Typography color='text.primary'>{data.from.name}</Typography>
|
||||
<Typography variant='body2'>{data.from.email}</Typography>
|
||||
</div>
|
||||
<CardHeaderAction data={data} isReplies={isReplies} />
|
||||
</div>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<div
|
||||
className={classnames('text-textSecondary', styles.message)}
|
||||
dangerouslySetInnerHTML={{ __html: data.message }}
|
||||
/>
|
||||
{data.attachments.length ? (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<hr className='border-be -mli-6 mbs-4' />
|
||||
<Typography variant='caption'>Attachments</Typography>
|
||||
{data.attachments.map(attachment => (
|
||||
<div key={attachment.fileName} className='flex items-center gap-2'>
|
||||
<img src={attachment.thumbnail} alt={attachment.fileName} className='bs-6' />
|
||||
<Typography>{attachment.fileName}</Typography>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default MailCard
|
||||
@@ -1,152 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { MouseEvent } from 'react'
|
||||
|
||||
// Types Imports
|
||||
import type { AppDispatch } from '@/redux-store'
|
||||
import type { EmailState } from '@/types/apps/emailTypes'
|
||||
|
||||
// Slice Imports
|
||||
import { moveEmailsToFolder, deleteTrashEmails, toggleReadEmails, toggleStarEmail } from '@/redux-store/slices/email'
|
||||
|
||||
// Component Imports
|
||||
import MailContentSearch from './MailContentSearch'
|
||||
import MailContentActions from './MailContentActions'
|
||||
import MailContentList from './MailContentList'
|
||||
import MailDetails from './MailDetails'
|
||||
|
||||
type Props = {
|
||||
folder?: string
|
||||
label?: string
|
||||
store: EmailState
|
||||
dispatch: AppDispatch
|
||||
uniqueLabels: string[]
|
||||
isInitialMount: boolean
|
||||
setSidebarOpen: (value: boolean) => void
|
||||
isBelowLgScreen: boolean
|
||||
isBelowMdScreen: boolean
|
||||
isBelowSmScreen: boolean
|
||||
setBackdropOpen: (value: boolean) => void
|
||||
}
|
||||
|
||||
const MailContent = (props: Props) => {
|
||||
// Props
|
||||
const {
|
||||
folder,
|
||||
label,
|
||||
store,
|
||||
dispatch,
|
||||
uniqueLabels,
|
||||
isInitialMount,
|
||||
setSidebarOpen,
|
||||
isBelowLgScreen,
|
||||
isBelowMdScreen,
|
||||
isBelowSmScreen,
|
||||
setBackdropOpen
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [selectedEmails, setSelectedEmails] = useState<Set<number>>(new Set())
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
const [reload, setReload] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
// Vars
|
||||
const emails = store.filteredEmails
|
||||
const currentEmail = emails.find(email => email.id === store.currentEmailId)
|
||||
|
||||
const areFilteredEmailsNone =
|
||||
emails.length === 0 ||
|
||||
emails.filter(
|
||||
email =>
|
||||
email.subject.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
email.from.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
).length === 0
|
||||
|
||||
// Action for deleting single email
|
||||
const handleSingleEmailDelete = (e: MouseEvent, emailId: number) => {
|
||||
e.stopPropagation()
|
||||
setSelectedEmails(prevSelectedEmails => {
|
||||
const newSelectedEmails = new Set(prevSelectedEmails)
|
||||
|
||||
newSelectedEmails.delete(emailId)
|
||||
|
||||
return newSelectedEmails
|
||||
})
|
||||
|
||||
if (folder === 'trash') {
|
||||
dispatch(deleteTrashEmails({ emailIds: [emailId] }))
|
||||
} else {
|
||||
dispatch(moveEmailsToFolder({ emailIds: [emailId], folder: 'trash' }))
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle read status for single email
|
||||
const handleToggleIsReadStatus = (e: MouseEvent, id: number) => {
|
||||
e.stopPropagation()
|
||||
dispatch(toggleReadEmails({ emailIds: [id] }))
|
||||
}
|
||||
|
||||
// Toggle star for single email
|
||||
const handleToggleStarEmail = (e: MouseEvent, id: number) => {
|
||||
e.stopPropagation()
|
||||
dispatch(toggleStarEmail({ emailId: id }))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center is-full bs-full relative overflow-hidden bg-backgroundPaper'>
|
||||
<MailContentSearch
|
||||
isBelowScreen={isBelowMdScreen}
|
||||
searchTerm={searchTerm}
|
||||
setSidebarOpen={setSidebarOpen}
|
||||
setBackdropOpen={setBackdropOpen}
|
||||
setSearchTerm={setSearchTerm}
|
||||
/>
|
||||
<MailContentActions
|
||||
areFilteredEmailsNone={areFilteredEmailsNone}
|
||||
selectedEmails={selectedEmails}
|
||||
setSelectedEmails={setSelectedEmails}
|
||||
emails={emails}
|
||||
folder={folder}
|
||||
label={label}
|
||||
uniqueLabels={uniqueLabels}
|
||||
setReload={setReload}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
<MailContentList
|
||||
isInitialMount={isInitialMount}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
isBelowLgScreen={isBelowLgScreen}
|
||||
reload={reload}
|
||||
areFilteredEmailsNone={areFilteredEmailsNone}
|
||||
searchTerm={searchTerm}
|
||||
selectedEmails={selectedEmails}
|
||||
dispatch={dispatch}
|
||||
store={store}
|
||||
emails={emails}
|
||||
folder={folder}
|
||||
setSelectedEmails={setSelectedEmails}
|
||||
setDrawerOpen={setDrawerOpen}
|
||||
handleToggleStarEmail={handleToggleStarEmail}
|
||||
handleSingleEmailDelete={handleSingleEmailDelete}
|
||||
handleToggleIsReadStatus={handleToggleIsReadStatus}
|
||||
/>
|
||||
<MailDetails
|
||||
drawerOpen={drawerOpen}
|
||||
setDrawerOpen={setDrawerOpen}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
isBelowLgScreen={isBelowLgScreen}
|
||||
currentEmail={currentEmail}
|
||||
emails={emails}
|
||||
folder={folder}
|
||||
label={label}
|
||||
dispatch={dispatch}
|
||||
handleSingleEmailDelete={handleSingleEmailDelete}
|
||||
handleToggleIsReadStatus={handleToggleIsReadStatus}
|
||||
handleToggleStarEmail={handleToggleStarEmail}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MailContent
|
||||
@@ -1,226 +0,0 @@
|
||||
// MUI Imports
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { AppDispatch } from '@/redux-store'
|
||||
import type { Email } from '@/types/apps/emailTypes'
|
||||
|
||||
// Slice Imports
|
||||
import { deleteTrashEmails, moveEmailsToFolder, toggleLabel, toggleReadEmails } from '@/redux-store/slices/email'
|
||||
|
||||
// Component Imports
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
// Data Imports
|
||||
import { labelColors } from './SidebarLeft'
|
||||
|
||||
type Props = {
|
||||
areFilteredEmailsNone: boolean
|
||||
selectedEmails: Set<number>
|
||||
emails: Email[]
|
||||
folder?: string
|
||||
label?: string
|
||||
uniqueLabels: string[]
|
||||
dispatch: AppDispatch
|
||||
setReload: (value: boolean) => void
|
||||
setSelectedEmails: (value: Set<number>) => void
|
||||
}
|
||||
|
||||
const MailContentActions = (props: Props) => {
|
||||
// Props
|
||||
const {
|
||||
areFilteredEmailsNone,
|
||||
selectedEmails,
|
||||
setSelectedEmails,
|
||||
emails,
|
||||
folder,
|
||||
label,
|
||||
uniqueLabels,
|
||||
setReload,
|
||||
dispatch
|
||||
} = props
|
||||
|
||||
// Vars
|
||||
const areAllSelected = selectedEmails.size > 0 && selectedEmails.size === emails.length
|
||||
const isIndeterminate = selectedEmails.size > 0 && selectedEmails.size < emails.length
|
||||
|
||||
// Handle reload
|
||||
const handleReload = () => {
|
||||
setReload(true)
|
||||
|
||||
setTimeout(() => {
|
||||
setReload(false)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// Toggle all emails' selection
|
||||
const handleSelectAllCheckboxes = () => {
|
||||
if (areAllSelected) {
|
||||
setSelectedEmails(new Set())
|
||||
} else {
|
||||
const visibleEmailIds = new Set(
|
||||
emails
|
||||
.filter(email => {
|
||||
if (folder === 'starred' && email.folder !== 'trash') {
|
||||
return email.isStarred
|
||||
} else if (label && uniqueLabels.includes(label) && email.folder !== 'trash') {
|
||||
return email.labels.includes(label)
|
||||
} else {
|
||||
return email.folder === folder
|
||||
}
|
||||
})
|
||||
.map(email => email.id)
|
||||
)
|
||||
|
||||
setSelectedEmails(visibleEmailIds)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete selected emails
|
||||
const handleEmailDelete = () => {
|
||||
const emailIds = emails.filter(email => selectedEmails.has(email.id)).map(email => email.id)
|
||||
|
||||
if (folder === 'trash') {
|
||||
dispatch(deleteTrashEmails({ emailIds }))
|
||||
setSelectedEmails(new Set())
|
||||
} else {
|
||||
dispatch(moveEmailsToFolder({ emailIds, folder: 'trash' }))
|
||||
setSelectedEmails(new Set())
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle all selected emails' read status
|
||||
const handleToggleAllReadEmails = () => {
|
||||
const emailIds = emails.filter(email => selectedEmails.has(email.id)).map(email => email.id)
|
||||
|
||||
dispatch(toggleReadEmails({ emailIds }))
|
||||
setSelectedEmails(new Set())
|
||||
}
|
||||
|
||||
// Move all selected emails to spam
|
||||
const handleMoveAllToSpam = () => {
|
||||
const emailIds = emails.filter(email => selectedEmails.has(email.id)).map(email => email.id)
|
||||
|
||||
dispatch(moveEmailsToFolder({ emailIds, folder: 'spam' }))
|
||||
setSelectedEmails(new Set())
|
||||
}
|
||||
|
||||
// Move all selected emails to inbox
|
||||
const handleMoveAllToInbox = () => {
|
||||
const emailIds = emails.filter(email => selectedEmails.has(email.id)).map(email => email.id)
|
||||
|
||||
dispatch(moveEmailsToFolder({ emailIds, folder: 'inbox' }))
|
||||
setSelectedEmails(new Set())
|
||||
}
|
||||
|
||||
// Handle click on label option from menu list
|
||||
const handleLabelClick = (label: string) => {
|
||||
const emailIds = emails.filter(email => selectedEmails.has(email.id)).map(email => email.id)
|
||||
|
||||
dispatch(toggleLabel({ emailIds, label }))
|
||||
setSelectedEmails(new Set())
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-between gap-4 max-sm:gap-0.5 is-full pli-4 plb-2 border-be'>
|
||||
<div className='flex items-center gap-1 max-sm:gap-0.5'>
|
||||
<Checkbox
|
||||
indeterminate={isIndeterminate}
|
||||
checked={areAllSelected}
|
||||
onChange={handleSelectAllCheckboxes}
|
||||
disabled={areFilteredEmailsNone}
|
||||
/>
|
||||
{(isIndeterminate || areAllSelected) && (
|
||||
<>
|
||||
<Tooltip title={folder === 'trash' ? 'Delete' : 'Move to trash'} placement='top'>
|
||||
<IconButton onClick={handleEmailDelete}>
|
||||
<i className='tabler-trash text-textSecondary' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={
|
||||
selectedEmails.size > 0 &&
|
||||
emails.filter(email => selectedEmails.has(email.id)).every(email => email.isRead)
|
||||
? 'Mark as unread'
|
||||
: 'Mark as read'
|
||||
}
|
||||
placement='top'
|
||||
>
|
||||
<IconButton onClick={handleToggleAllReadEmails}>
|
||||
<i
|
||||
className={classnames(
|
||||
'text-textSecondary',
|
||||
selectedEmails.size > 0 &&
|
||||
emails.filter(email => selectedEmails.has(email.id)).every(email => email.isRead)
|
||||
? 'tabler-mail'
|
||||
: 'tabler-mail-opened'
|
||||
)}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{folder === 'inbox' && (
|
||||
<Tooltip title='Move to spam' placement='top'>
|
||||
<IconButton onClick={handleMoveAllToSpam}>
|
||||
<i className='tabler-info-circle text-textSecondary' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{folder === 'spam' && (
|
||||
<Tooltip title='Move to inbox' placement='top'>
|
||||
<IconButton onClick={handleMoveAllToInbox}>
|
||||
<i className='tabler-inbox text-textSecondary' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{folder === 'trash' && (
|
||||
<OptionMenu
|
||||
tooltipProps={{ title: 'Move to folder', placement: 'top' }}
|
||||
icon={<i className='tabler-folder text-textSecondary' />}
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
options={[
|
||||
{
|
||||
text: 'Spam',
|
||||
icon: <i className='tabler-info-circle' />,
|
||||
menuItemProps: { onClick: handleMoveAllToSpam }
|
||||
},
|
||||
{
|
||||
text: 'Inbox',
|
||||
icon: <i className='tabler-inbox' />,
|
||||
menuItemProps: { onClick: handleMoveAllToInbox }
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<OptionMenu
|
||||
tooltipProps={{ title: 'Toggle label', placement: 'top' }}
|
||||
icon={<i className='tabler-tag text-textSecondary' />}
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
options={Object.entries(labelColors).map(([key, value]) => ({
|
||||
text: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
menuItemProps: { onClick: () => handleLabelClick(key) },
|
||||
icon: <i className={`tabler-circle-filled text-xs text-${value.color}`} />
|
||||
}))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex gap-1 max-sm:gap-0.5'>
|
||||
<Tooltip title='Refresh' placement='top'>
|
||||
<IconButton onClick={handleReload}>
|
||||
<i className='tabler-refresh text-textSecondary' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton>
|
||||
<i className='tabler-dots-vertical text-textSecondary' />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MailContentActions
|
||||
@@ -1,230 +0,0 @@
|
||||
// React Imports
|
||||
import type { Dispatch, MouseEvent, ReactNode, SetStateAction } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import Backdrop from '@mui/material/Backdrop'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// Type Imports
|
||||
import type { AppDispatch } from '@/redux-store'
|
||||
import type { Email, EmailState } from '@/types/apps/emailTypes'
|
||||
|
||||
// Slice Imports
|
||||
import { getCurrentEmail, moveEmailsToFolder } from '@/redux-store/slices/email'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
|
||||
// Styles Imports
|
||||
import styles from './styles.module.css'
|
||||
|
||||
// Data Imports
|
||||
import { labelColors } from './SidebarLeft'
|
||||
|
||||
type Props = {
|
||||
isInitialMount: boolean
|
||||
isBelowSmScreen: boolean
|
||||
isBelowLgScreen: boolean
|
||||
reload: boolean
|
||||
areFilteredEmailsNone: boolean
|
||||
searchTerm: string
|
||||
selectedEmails: Set<number>
|
||||
dispatch: AppDispatch
|
||||
store: EmailState
|
||||
emails: Email[]
|
||||
folder?: string
|
||||
setSelectedEmails: Dispatch<SetStateAction<Set<number>>> // This type has been written to solve type error in this file
|
||||
setDrawerOpen: (value: boolean) => void
|
||||
handleToggleStarEmail: (e: MouseEvent, id: number) => void
|
||||
handleSingleEmailDelete: (e: MouseEvent, id: number) => void
|
||||
handleToggleIsReadStatus: (e: MouseEvent, id: number) => void
|
||||
}
|
||||
|
||||
const ScrollWrapper = ({ children, isBelowLgScreen }: { children: ReactNode; isBelowLgScreen: boolean }) => {
|
||||
if (isBelowLgScreen) {
|
||||
return <div className='bs-full overflow-y-auto overflow-x-hidden relative'>{children}</div>
|
||||
} else {
|
||||
return <PerfectScrollbar options={{ wheelPropagation: false }}>{children}</PerfectScrollbar>
|
||||
}
|
||||
}
|
||||
|
||||
const MailContentList = (props: Props) => {
|
||||
// Props
|
||||
const {
|
||||
isInitialMount,
|
||||
isBelowSmScreen,
|
||||
isBelowLgScreen,
|
||||
reload,
|
||||
areFilteredEmailsNone,
|
||||
searchTerm,
|
||||
selectedEmails,
|
||||
dispatch,
|
||||
store,
|
||||
emails,
|
||||
folder,
|
||||
setSelectedEmails,
|
||||
setDrawerOpen,
|
||||
handleToggleStarEmail,
|
||||
handleSingleEmailDelete,
|
||||
handleToggleIsReadStatus
|
||||
} = props
|
||||
|
||||
// Toggle single selection of email
|
||||
const toggleEmailSelected = (emailId: number) => {
|
||||
setSelectedEmails(prevSelectedEmails => {
|
||||
const newSelectedEmails = new Set(prevSelectedEmails)
|
||||
|
||||
if (newSelectedEmails.has(emailId)) {
|
||||
newSelectedEmails.delete(emailId)
|
||||
} else {
|
||||
newSelectedEmails.add(emailId)
|
||||
}
|
||||
|
||||
return newSelectedEmails
|
||||
})
|
||||
}
|
||||
|
||||
// Move single email to spam
|
||||
const handleMoveToSpam = (e: MouseEvent, id: number) => {
|
||||
e.stopPropagation()
|
||||
dispatch(moveEmailsToFolder({ emailIds: [id], folder: 'spam' }))
|
||||
}
|
||||
|
||||
// Handle email click
|
||||
const handleEmailClick = (id: number) => {
|
||||
setDrawerOpen(true)
|
||||
|
||||
if (store.currentEmailId !== id || emails.find(email => email.id === id)?.isRead === false) {
|
||||
dispatch(getCurrentEmail(id))
|
||||
}
|
||||
}
|
||||
|
||||
return isInitialMount ? (
|
||||
<div className='flex items-center justify-center gap-2 grow is-full'>
|
||||
<CircularProgress />
|
||||
<Typography>Loading...</Typography>
|
||||
</div>
|
||||
) : areFilteredEmailsNone ? (
|
||||
<div className='relative flex justify-center gap-2 grow is-full bg-backgroundPaper'>
|
||||
<Typography className='m-3'>No emails found!</Typography>
|
||||
{reload && (
|
||||
<Backdrop open={reload} className='absolute text-white z-10 bg-textDisabled'>
|
||||
<CircularProgress color='inherit' />
|
||||
</Backdrop>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className='relative overflow-hidden grow is-full'>
|
||||
<ScrollWrapper isBelowLgScreen={isBelowLgScreen}>
|
||||
<div className='flex flex-col'>
|
||||
{emails
|
||||
.filter(
|
||||
email =>
|
||||
email.subject.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
email.from.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
.map(email => (
|
||||
<div
|
||||
key={email.id}
|
||||
className={classnames('p-4 cursor-pointer', styles.emailList, { 'bg-actionHover': email.isRead })}
|
||||
onClick={() => handleEmailClick(email.id)}
|
||||
>
|
||||
<div className='flex items-center justify-between gap-2'>
|
||||
<div className='flex items-center gap-2 overflow-hidden'>
|
||||
<Checkbox
|
||||
checked={selectedEmails.has(email.id)}
|
||||
onChange={() => toggleEmailSelected(email.id)}
|
||||
onClick={e => e.stopPropagation()}
|
||||
/>
|
||||
<IconButton onClick={e => handleToggleStarEmail(e, email.id)}>
|
||||
<i
|
||||
className={classnames('tabler-star', email.isStarred ? 'text-warning' : 'text-textSecondary')}
|
||||
/>
|
||||
</IconButton>
|
||||
<CustomAvatar src={email.from.avatar} alt={email.from.name} size={32} />
|
||||
<div className='flex gap-4 justify-between items-center overflow-hidden'>
|
||||
<Typography className='font-medium whitespace-nowrap' color='text.primary'>
|
||||
{email.from.name}
|
||||
</Typography>
|
||||
<Typography variant='body2' noWrap>
|
||||
{email.subject}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
{!isBelowSmScreen && (
|
||||
<div
|
||||
className={classnames('flex items-center gap-2', styles.emailInfo, {
|
||||
[styles.show]: isBelowLgScreen
|
||||
})}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
{email.labels.map(label => (
|
||||
<i
|
||||
key={label}
|
||||
className={classnames('tabler-circle-filled text-[10px]', labelColors[label].colorClass)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Typography variant='body2' color='text.disabled' className='whitespace-nowrap'>
|
||||
{new Intl.DateTimeFormat('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
}).format(new Date(email.time))}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{!isBelowLgScreen && (
|
||||
<div className={styles.emailActions}>
|
||||
<Tooltip title={folder === 'trash' ? 'Delete' : 'Move to trash'} placement='top'>
|
||||
<IconButton onClick={e => handleSingleEmailDelete(e, email.id)}>
|
||||
<i className='tabler-trash text-textSecondary' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={email.isRead ? 'Mark as unread' : 'Mark as read'} placement='top'>
|
||||
<IconButton
|
||||
onClick={e => {
|
||||
handleToggleIsReadStatus(e, email.id)
|
||||
setSelectedEmails(new Set())
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className={classnames(
|
||||
'text-textSecondary',
|
||||
email.isRead ? 'tabler-mail' : 'tabler-mail-opened'
|
||||
)}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{(folder === 'inbox' || folder === 'trash') && (
|
||||
<Tooltip title='Move to spam' placement='top'>
|
||||
<IconButton onClick={e => handleMoveToSpam(e, email.id)}>
|
||||
<i className='tabler-info-circle text-textSecondary' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollWrapper>
|
||||
{reload && (
|
||||
<Backdrop open={reload} className='absolute text-white z-10 bg-textDisabled'>
|
||||
<CircularProgress color='inherit' />
|
||||
</Backdrop>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MailContentList
|
||||
@@ -1,42 +0,0 @@
|
||||
// MUI Imports
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import InputBase from '@mui/material/InputBase'
|
||||
|
||||
type Props = {
|
||||
isBelowScreen: boolean
|
||||
searchTerm: string
|
||||
setSidebarOpen: (value: boolean) => void
|
||||
setBackdropOpen: (value: boolean) => void
|
||||
setSearchTerm: (value: string) => void
|
||||
}
|
||||
|
||||
const MailContentSearch = (props: Props) => {
|
||||
// Props
|
||||
const { isBelowScreen, searchTerm, setSidebarOpen, setBackdropOpen, setSearchTerm } = props
|
||||
|
||||
// Open sidebar on below md screen
|
||||
const handleToggleSidebar = () => {
|
||||
setSidebarOpen(true)
|
||||
setBackdropOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-1 is-full pli-4 border-be'>
|
||||
{isBelowScreen && (
|
||||
<IconButton onClick={handleToggleSidebar}>
|
||||
<i className='tabler-menu-2 text-textSecondary' />
|
||||
</IconButton>
|
||||
)}
|
||||
<InputBase
|
||||
fullWidth
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
startAdornment={<i className='tabler-search text-textSecondary mie-4' />}
|
||||
placeholder='Search mail'
|
||||
className='bs-[56px]'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MailContentSearch
|
||||
@@ -1,477 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { MouseEvent, ReactNode } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Typography from '@mui/material/Typography'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import CardActions from '@mui/material/CardActions'
|
||||
import Button from '@mui/material/Button'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import { styled } from '@mui/material'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import { useEditor, EditorContent } from '@tiptap/react'
|
||||
import { StarterKit } from '@tiptap/starter-kit'
|
||||
import { Underline } from '@tiptap/extension-underline'
|
||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||
import { TextAlign } from '@tiptap/extension-text-align'
|
||||
import type { Editor } from '@tiptap/core'
|
||||
|
||||
// Types Imports
|
||||
import type { AppDispatch } from '@/redux-store'
|
||||
import type { Email } from '@/types/apps/emailTypes'
|
||||
|
||||
// Slice Imports
|
||||
import { moveEmailsToFolder, navigateEmails, toggleLabel } from '@/redux-store/slices/email'
|
||||
|
||||
// Components Imports
|
||||
import CustomIconButton from '@core/components/mui/IconButton'
|
||||
import CustomChip from '@core/components/mui/Chip'
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
import DirectionalIcon from '@components/DirectionalIcon'
|
||||
import MailCard from './MailCard'
|
||||
|
||||
// Styles Imports
|
||||
import styles from './styles.module.css'
|
||||
|
||||
// Data Imports
|
||||
import { labelColors } from './SidebarLeft'
|
||||
|
||||
type Props = {
|
||||
drawerOpen: boolean
|
||||
setDrawerOpen: (value: boolean) => void
|
||||
currentEmail?: Email
|
||||
isBelowSmScreen: boolean
|
||||
isBelowLgScreen: boolean
|
||||
emails: Email[]
|
||||
folder?: string
|
||||
label?: string
|
||||
dispatch: AppDispatch
|
||||
handleSingleEmailDelete: (e: MouseEvent, emailIds: number) => void
|
||||
handleToggleIsReadStatus: (e: MouseEvent, emailId: number) => void
|
||||
handleToggleStarEmail: (e: MouseEvent, emailId: number) => void
|
||||
}
|
||||
|
||||
const ScrollWrapper = ({ children, isBelowLgScreen }: { children: ReactNode; isBelowLgScreen: boolean }) => {
|
||||
if (isBelowLgScreen) {
|
||||
return <div className='bs-full overflow-y-auto overflow-x-hidden bg-actionHover'>{children}</div>
|
||||
} else {
|
||||
return (
|
||||
<PerfectScrollbar className='bg-actionHover' options={{ wheelPropagation: false }}>
|
||||
{children}
|
||||
</PerfectScrollbar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const DetailsDrawer = styled('div')<{ drawerOpen: boolean }>(({ drawerOpen }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
blockSize: '100%',
|
||||
inlineSize: '100%',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: drawerOpen ? 0 : '-100%',
|
||||
zIndex: 11,
|
||||
overflow: 'hidden',
|
||||
background: 'var(--mui-palette-background-paper)',
|
||||
transition: 'right 0.3s ease'
|
||||
}))
|
||||
|
||||
const EditorToolbar = ({ editor }: { editor: Editor | null }) => {
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap gap-x-3 gap-y-1 pli-6'>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive('bold') && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
>
|
||||
<i className={classnames('tabler-bold', { 'text-textPrimary': !editor.isActive('bold') })} />
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive('underline') && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
>
|
||||
<i className={classnames('tabler-underline', { 'text-textPrimary': !editor.isActive('underline') })} />
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive('italic') && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
>
|
||||
<i className={classnames('tabler-italic', { 'text-textPrimary': !editor.isActive('italic') })} />
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive('strike') && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
>
|
||||
<i className={classnames('tabler-strikethrough', { 'text-textPrimary': !editor.isActive('strike') })} />
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive({ textAlign: 'left' }) && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||
>
|
||||
<i
|
||||
className={classnames('tabler-align-left', { 'text-textPrimary': !editor.isActive({ textAlign: 'left' }) })}
|
||||
/>
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive({ textAlign: 'center' }) && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||
>
|
||||
<i
|
||||
className={classnames('tabler-align-center', {
|
||||
'text-textPrimary': !editor.isActive({ textAlign: 'center' })
|
||||
})}
|
||||
/>
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive({ textAlign: 'right' }) && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||
>
|
||||
<i
|
||||
className={classnames('tabler-align-right', {
|
||||
'text-textPrimary': !editor.isActive({ textAlign: 'right' })
|
||||
})}
|
||||
/>
|
||||
</CustomIconButton>
|
||||
<CustomIconButton
|
||||
{...(editor.isActive({ textAlign: 'justify' }) && { color: 'primary' })}
|
||||
variant='tonal'
|
||||
size='small'
|
||||
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
||||
>
|
||||
<i
|
||||
className={classnames('tabler-align-justified', {
|
||||
'text-textPrimary': !editor.isActive({ textAlign: 'justify' })
|
||||
})}
|
||||
/>
|
||||
</CustomIconButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MailDetails = (props: Props) => {
|
||||
// Props
|
||||
const {
|
||||
drawerOpen,
|
||||
setDrawerOpen,
|
||||
isBelowSmScreen,
|
||||
isBelowLgScreen,
|
||||
currentEmail,
|
||||
emails,
|
||||
folder,
|
||||
label,
|
||||
dispatch,
|
||||
handleSingleEmailDelete,
|
||||
handleToggleIsReadStatus,
|
||||
handleToggleStarEmail
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [showReplies, setShowReplies] = useState(false)
|
||||
const [reply, setReply] = useState(false)
|
||||
|
||||
// Handle navigation between emails and reset reply state
|
||||
const handleEmailNavigation = (type: 'next' | 'prev') => {
|
||||
dispatch(navigateEmails({ type, emails, currentEmailId: currentEmail?.id }))
|
||||
|
||||
if (reply) {
|
||||
setReply(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Close drawer and reset reply state
|
||||
const handleCloseDrawer = () => {
|
||||
setDrawerOpen(false)
|
||||
|
||||
if (reply) {
|
||||
setReply(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Move all selected emails to spam
|
||||
const handleMoveAllToSpam = () => {
|
||||
dispatch(moveEmailsToFolder({ emailIds: [currentEmail?.id], folder: 'spam' }))
|
||||
setDrawerOpen(false)
|
||||
}
|
||||
|
||||
// Move all selected emails to inbox
|
||||
const handleMoveAllToInbox = () => {
|
||||
dispatch(moveEmailsToFolder({ emailIds: [currentEmail?.id], folder: 'inbox' }))
|
||||
setDrawerOpen(false)
|
||||
}
|
||||
|
||||
// Handle click on label option from menu list
|
||||
const handleLabelClick = (value: string) => {
|
||||
dispatch(toggleLabel({ emailIds: [currentEmail?.id], label: value }))
|
||||
label === value && setDrawerOpen(false)
|
||||
}
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Placeholder.configure({
|
||||
placeholder: 'Write your message...'
|
||||
}),
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph']
|
||||
}),
|
||||
Underline
|
||||
],
|
||||
immediatelyRender: false
|
||||
})
|
||||
|
||||
return (
|
||||
<DetailsDrawer drawerOpen={drawerOpen}>
|
||||
{currentEmail && (
|
||||
<>
|
||||
<div className='plb-4 pli-6'>
|
||||
<div className='flex justify-between gap-2'>
|
||||
<div className='flex gap-2 items-center overflow-hidden'>
|
||||
<IconButton onClick={handleCloseDrawer}>
|
||||
<DirectionalIcon
|
||||
ltrIconClass='tabler-chevron-left'
|
||||
rtlIconClass='tabler-chevron-right'
|
||||
className='text-textSecondary'
|
||||
/>
|
||||
</IconButton>
|
||||
<div className='flex items-center flex-wrap gap-2 overflow-hidden'>
|
||||
<Typography color='text.primary' noWrap>
|
||||
{currentEmail.subject}
|
||||
</Typography>
|
||||
<div className='flex items-center flex-wrap gap-2'>
|
||||
{currentEmail.labels && currentEmail.labels.length
|
||||
? currentEmail.labels.map(label => {
|
||||
return (
|
||||
<CustomChip
|
||||
key={label}
|
||||
variant='tonal'
|
||||
round='true'
|
||||
size='small'
|
||||
label={label}
|
||||
color={labelColors[label].color}
|
||||
className='capitalize'
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<IconButton disabled={currentEmail.id === emails[0].id} onClick={() => handleEmailNavigation('prev')}>
|
||||
<DirectionalIcon
|
||||
ltrIconClass='tabler-chevron-left'
|
||||
rtlIconClass='tabler-chevron-right'
|
||||
className='text-textSecondary'
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
disabled={currentEmail.id === emails[emails.length - 1].id}
|
||||
onClick={() => handleEmailNavigation('next')}
|
||||
>
|
||||
<DirectionalIcon
|
||||
ltrIconClass='tabler-chevron-right'
|
||||
rtlIconClass='tabler-chevron-left'
|
||||
className='text-textSecondary'
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-between gap-4 plb-2 pli-6 border-y'>
|
||||
<div className='flex gap-1'>
|
||||
<Tooltip title={folder === 'trash' ? 'Delete' : 'Move to trash'} placement='top'>
|
||||
<IconButton
|
||||
onClick={e => {
|
||||
setDrawerOpen(false)
|
||||
handleSingleEmailDelete(e, currentEmail.id)
|
||||
}}
|
||||
>
|
||||
<i className='tabler-trash text-textSecondary' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title='Mark as unread' placement='top'>
|
||||
<IconButton
|
||||
onClick={e => {
|
||||
setDrawerOpen(false)
|
||||
handleToggleIsReadStatus(e, currentEmail.id)
|
||||
}}
|
||||
>
|
||||
<i className='tabler-mail text-textSecondary' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{folder === 'inbox' && (
|
||||
<Tooltip title='Move to spam' placement='top'>
|
||||
<IconButton onClick={handleMoveAllToSpam}>
|
||||
<i className='tabler-info-circle text-textSecondary' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{folder === 'spam' && (
|
||||
<Tooltip title='Move to inbox' placement='top'>
|
||||
<IconButton onClick={handleMoveAllToInbox}>
|
||||
<i className='tabler-inbox text-textSecondary' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{folder === 'trash' && (
|
||||
<OptionMenu
|
||||
tooltipProps={{ title: 'Move to folder', placement: 'top' }}
|
||||
icon={<i className='tabler-folder text-textSecondary' />}
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
options={[
|
||||
{
|
||||
text: 'Spam',
|
||||
icon: <i className='tabler-info-circle' />,
|
||||
menuItemProps: { onClick: handleMoveAllToSpam }
|
||||
},
|
||||
{
|
||||
text: 'Inbox',
|
||||
icon: <i className='tabler-inbox' />,
|
||||
menuItemProps: { onClick: handleMoveAllToInbox }
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<OptionMenu
|
||||
tooltipProps={{ title: 'Toggle label', placement: 'top' }}
|
||||
icon={<i className='tabler-tag text-textSecondary' />}
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
options={Object.entries(labelColors).map(([key, value]) => ({
|
||||
text: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
menuItemProps: { onClick: () => handleLabelClick(key) },
|
||||
icon: <i className={`tabler-circle-filled text-xs text-${value.color}`} />
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex gap-1'>
|
||||
<IconButton
|
||||
onClick={e => {
|
||||
handleToggleStarEmail(e, currentEmail.id)
|
||||
folder === 'starred' && setDrawerOpen(false)
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className={classnames('tabler-star', currentEmail.isStarred ? 'text-warning' : 'text-textSecondary')}
|
||||
/>
|
||||
</IconButton>
|
||||
{currentEmail.replies.length ? (
|
||||
<IconButton onClick={() => setShowReplies(!showReplies)}>
|
||||
<i
|
||||
className={classnames('text-textSecondary', {
|
||||
'tabler-arrows-move-vertical': !showReplies,
|
||||
'tabler-fold': showReplies
|
||||
})}
|
||||
/>
|
||||
</IconButton>
|
||||
) : null}
|
||||
<IconButton>
|
||||
<i className='tabler-dots-vertical text-textSecondary' />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollWrapper isBelowLgScreen={isBelowLgScreen}>
|
||||
<div className='plb-5 pli-8 flex flex-col gap-4'>
|
||||
{currentEmail.replies.length && !showReplies ? (
|
||||
<Typography className='self-center text-center cursor-pointer' onClick={() => setShowReplies(true)}>
|
||||
{`${currentEmail.replies.length} Earlier Messages`}
|
||||
</Typography>
|
||||
) : null}
|
||||
{showReplies
|
||||
? currentEmail.replies.map(reply => <MailCard key={reply.id} data={reply} isReplies={false} />)
|
||||
: null}
|
||||
|
||||
<div>
|
||||
{!showReplies && currentEmail.replies.length ? (
|
||||
<>
|
||||
<div
|
||||
className={classnames(styles.mailReplyLayer, styles.layer1)}
|
||||
onClick={() => setShowReplies(true)}
|
||||
/>
|
||||
<div
|
||||
className={classnames(styles.mailReplyLayer, styles.layer2)}
|
||||
onClick={() => setShowReplies(true)}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<MailCard data={currentEmail} isReplies={true} />
|
||||
<Card className='border mbs-4'>
|
||||
{!reply ? (
|
||||
<CardContent>
|
||||
<Typography>
|
||||
Click here to
|
||||
<span className='text-primary cursor-pointer mli-1' onClick={() => setReply(true)}>
|
||||
Reply
|
||||
</span>
|
||||
or
|
||||
<span className='text-primary cursor-pointer mis-1'>Forward</span>
|
||||
</Typography>
|
||||
</CardContent>
|
||||
) : (
|
||||
<div className='flex flex-col gap-y-6'>
|
||||
<CardContent className='pbe-0'>
|
||||
<Typography color='text.primary'>{`Reply to ${currentEmail.from.name}`}</Typography>
|
||||
</CardContent>
|
||||
<div>
|
||||
<EditorToolbar editor={editor} />
|
||||
<EditorContent editor={editor} className='overflow-y-auto' />
|
||||
</div>
|
||||
<CardActions className='flex items-center justify-end pbs-0'>
|
||||
<IconButton>
|
||||
<i className='tabler-trash text-textSecondary' onClick={() => setReply(false)} />
|
||||
</IconButton>
|
||||
{isBelowSmScreen ? (
|
||||
<CustomIconButton color='secondary'>
|
||||
<i className='tabler-paperclip text-textPrimary' />
|
||||
</CustomIconButton>
|
||||
) : (
|
||||
<Button color='secondary' startIcon={<i className='tabler-paperclip text-textPrimary' />}>
|
||||
Attachments
|
||||
</Button>
|
||||
)}
|
||||
{isBelowSmScreen ? (
|
||||
<CustomIconButton variant='contained' color='primary'>
|
||||
<i className='tabler-send' />
|
||||
</CustomIconButton>
|
||||
) : (
|
||||
<Button variant='contained' color='primary' endIcon={<i className='tabler-send' />}>
|
||||
Send
|
||||
</Button>
|
||||
)}
|
||||
</CardActions>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollWrapper>
|
||||
</>
|
||||
)}
|
||||
</DetailsDrawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default MailDetails
|
||||
@@ -1,195 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
|
||||
// MUI Imports
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Button from '@mui/material/Button'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// Types Imports
|
||||
import type { Email, EmailState } from '@/types/apps/emailTypes'
|
||||
import type { ThemeColor } from '@core/types'
|
||||
import type { Locale } from '@/configs/i18n'
|
||||
|
||||
// Components Imports
|
||||
import ComposeMail from './ComposeMail'
|
||||
import CustomChip from '@core/components/mui/Chip'
|
||||
|
||||
// Util Imports
|
||||
import { getLocalizedUrl } from '@/utils/i18n'
|
||||
|
||||
// Styles Imports
|
||||
import styles from './styles.module.css'
|
||||
|
||||
type Props = {
|
||||
store: EmailState
|
||||
isBelowLgScreen: boolean
|
||||
isBelowMdScreen: boolean
|
||||
isBelowSmScreen: boolean
|
||||
sidebarOpen: boolean
|
||||
setSidebarOpen: (value: boolean) => void
|
||||
uniqueLabels: string[]
|
||||
folder?: string
|
||||
label: string
|
||||
}
|
||||
|
||||
type LabelColor = {
|
||||
color: ThemeColor
|
||||
colorClass: string
|
||||
}
|
||||
|
||||
// Constants
|
||||
const icons = {
|
||||
inbox: 'tabler-mail',
|
||||
sent: 'tabler-send',
|
||||
draft: 'tabler-edit',
|
||||
starred: 'tabler-star',
|
||||
spam: 'tabler-alert-octagon',
|
||||
trash: 'tabler-trash'
|
||||
}
|
||||
|
||||
export const labelColors: { [key: string]: LabelColor } = {
|
||||
personal: { color: 'success', colorClass: 'text-success' },
|
||||
company: { color: 'primary', colorClass: 'text-primary' },
|
||||
important: { color: 'warning', colorClass: 'text-warning' },
|
||||
private: { color: 'error', colorClass: 'text-error' }
|
||||
}
|
||||
|
||||
const ScrollWrapper = ({ children, isBelowLgScreen }: { children: ReactNode; isBelowLgScreen: boolean }) => {
|
||||
if (isBelowLgScreen) {
|
||||
return <div className='bs-full overflow-y-auto overflow-x-hidden'>{children}</div>
|
||||
} else {
|
||||
return <PerfectScrollbar options={{ wheelPropagation: false }}>{children}</PerfectScrollbar>
|
||||
}
|
||||
}
|
||||
|
||||
const SidebarLeft = (props: Props) => {
|
||||
// Props
|
||||
const {
|
||||
store,
|
||||
isBelowLgScreen,
|
||||
isBelowMdScreen,
|
||||
isBelowSmScreen,
|
||||
sidebarOpen,
|
||||
setSidebarOpen,
|
||||
uniqueLabels,
|
||||
folder,
|
||||
label
|
||||
} = props
|
||||
|
||||
// States
|
||||
const [openCompose, setOpenCompose] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
const folderCounts = store.emails.reduce((counts: Record<string, number>, email: Email) => {
|
||||
if (!email.isRead && email.folder !== 'trash') {
|
||||
counts[email.folder] = (counts[email.folder] || 0) + 1
|
||||
} else if (email.folder === 'draft') {
|
||||
counts.draft = (counts.draft || 0) + 1
|
||||
}
|
||||
|
||||
return counts
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
open={sidebarOpen}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
className='bs-full'
|
||||
variant={!isBelowMdScreen ? 'permanent' : 'persistent'}
|
||||
ModalProps={{ disablePortal: true, keepMounted: true }}
|
||||
sx={{
|
||||
zIndex: isBelowMdScreen && sidebarOpen ? 11 : 10,
|
||||
position: !isBelowMdScreen ? 'static' : 'absolute',
|
||||
'& .MuiDrawer-paper': {
|
||||
boxShadow: 'none',
|
||||
overflow: 'hidden',
|
||||
width: '260px',
|
||||
position: !isBelowMdScreen ? 'static' : 'absolute'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Button color='primary' variant='contained' fullWidth onClick={() => setOpenCompose(true)}>
|
||||
Compose
|
||||
</Button>
|
||||
</CardContent>
|
||||
<ScrollWrapper isBelowLgScreen={isBelowLgScreen}>
|
||||
<div className='flex flex-col gap-1 plb-4'>
|
||||
{Object.entries(icons).map(([key, value]) => (
|
||||
<Link
|
||||
key={key}
|
||||
href={getLocalizedUrl(`/apps/email/${key}`, locale as Locale)}
|
||||
prefetch
|
||||
className={classnames('flex items-center justify-between plb-1 pli-6 gap-2.5 min-bs-8 cursor-pointer', {
|
||||
[styles.activeSidebarListItem]: key === folder && !label
|
||||
})}
|
||||
>
|
||||
<div className='flex items-center gap-2.5'>
|
||||
<i className={classnames(value, 'text-xl')} />
|
||||
<Typography className='capitalize' color='inherit'>
|
||||
{key}
|
||||
</Typography>
|
||||
</div>
|
||||
{folderCounts[key] && (
|
||||
<CustomChip
|
||||
label={folderCounts[key]}
|
||||
size='small'
|
||||
round='true'
|
||||
variant='tonal'
|
||||
color={
|
||||
key === 'inbox' ? 'primary' : key === 'draft' ? 'warning' : key === 'spam' ? 'error' : 'default'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex flex-col gap-4 plb-4'>
|
||||
<Typography variant='caption' className='uppercase pli-6'>
|
||||
Labels
|
||||
</Typography>
|
||||
<div className='flex flex-col gap-3'>
|
||||
{uniqueLabels.map(labelName => (
|
||||
<Link
|
||||
key={labelName}
|
||||
href={getLocalizedUrl(`/apps/email/label/${labelName}`, locale as Locale)}
|
||||
prefetch
|
||||
className={classnames('flex items-center gap-x-2 pli-6 cursor-pointer', {
|
||||
[styles.activeSidebarListItem]: labelName === label
|
||||
})}
|
||||
>
|
||||
<i className={classnames('tabler-circle-filled text-xs', labelColors[labelName].colorClass)} />
|
||||
<Typography className='capitalize' color='inherit'>
|
||||
{labelName}
|
||||
</Typography>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollWrapper>
|
||||
</Drawer>
|
||||
<ComposeMail
|
||||
openCompose={openCompose}
|
||||
setOpenCompose={setOpenCompose}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
isBelowMdScreen={isBelowMdScreen}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SidebarLeft
|
||||
@@ -1,125 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import { useMediaQuery } from '@mui/material'
|
||||
import Backdrop from '@mui/material/Backdrop'
|
||||
import type { Theme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
// Type Imports
|
||||
import type { RootState } from '@/redux-store'
|
||||
|
||||
// Slice Imports
|
||||
import { filterEmails } from '@/redux-store/slices/email'
|
||||
|
||||
// Component Imports
|
||||
import SidebarLeft from './SidebarLeft'
|
||||
import MailContent from './MailContent'
|
||||
|
||||
// Hook Imports
|
||||
import { useSettings } from '@core/hooks/useSettings'
|
||||
|
||||
// Util Imports
|
||||
import { commonLayoutClasses } from '@layouts/utils/layoutClasses'
|
||||
|
||||
const EmailWrapper = ({ folder, label }: { folder?: string; label?: string }) => {
|
||||
// States
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const [backdropOpen, setBackdropOpen] = useState(false)
|
||||
|
||||
// Refs
|
||||
const isInitialMount = useRef(true)
|
||||
|
||||
// Hooks
|
||||
const { settings } = useSettings()
|
||||
const emailStore = useSelector((state: RootState) => state.emailReducer)
|
||||
const dispatch = useDispatch()
|
||||
const isBelowLgScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('lg'))
|
||||
const isBelowMdScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'))
|
||||
const isBelowSmScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
|
||||
|
||||
// Vars
|
||||
const uniqueLabels = [...new Set(emailStore.emails.flatMap(email => email.labels))]
|
||||
|
||||
// Handle backdrop on click
|
||||
const handleBackdropClick = () => {
|
||||
setSidebarOpen(false)
|
||||
setBackdropOpen(false)
|
||||
}
|
||||
|
||||
// Set loading false on initial mount
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Filter all emails based on folder and label
|
||||
useEffect(() => {
|
||||
dispatch(filterEmails({ emails: emailStore.emails, folder, label, uniqueLabels }))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [emailStore.emails, folder, label])
|
||||
|
||||
// Hide backdrop when left sidebar is closed
|
||||
useEffect(() => {
|
||||
if (backdropOpen && !sidebarOpen) {
|
||||
setBackdropOpen(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sidebarOpen])
|
||||
|
||||
// Hide backdrop when screen size is above md
|
||||
useEffect(() => {
|
||||
if (backdropOpen && !isBelowMdScreen) {
|
||||
setBackdropOpen(false)
|
||||
}
|
||||
|
||||
if (sidebarOpen && !isBelowMdScreen) {
|
||||
setSidebarOpen(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isBelowMdScreen])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(commonLayoutClasses.contentHeightFixed, 'flex is-full overflow-hidden rounded relative', {
|
||||
border: settings.skin === 'bordered',
|
||||
'shadow-md': settings.skin !== 'bordered'
|
||||
})}
|
||||
>
|
||||
<SidebarLeft
|
||||
store={emailStore}
|
||||
isBelowLgScreen={isBelowLgScreen}
|
||||
isBelowMdScreen={isBelowMdScreen}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
sidebarOpen={sidebarOpen}
|
||||
setSidebarOpen={setSidebarOpen}
|
||||
folder={folder}
|
||||
uniqueLabels={uniqueLabels}
|
||||
label={label || ''}
|
||||
/>
|
||||
<Backdrop open={backdropOpen} onClick={handleBackdropClick} className='absolute z-10' />
|
||||
<MailContent
|
||||
store={emailStore}
|
||||
dispatch={dispatch}
|
||||
folder={folder}
|
||||
label={label}
|
||||
uniqueLabels={uniqueLabels}
|
||||
isInitialMount={isInitialMount.current}
|
||||
setSidebarOpen={setSidebarOpen}
|
||||
isBelowLgScreen={isBelowLgScreen}
|
||||
isBelowMdScreen={isBelowMdScreen}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
setBackdropOpen={setBackdropOpen}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmailWrapper
|
||||
@@ -1,58 +0,0 @@
|
||||
.activeSidebarListItem {
|
||||
color: var(--mui-palette-primary-main);
|
||||
border-inline-start: 3px solid var(--mui-palette-primary-main);
|
||||
padding-inline-start: 1.3125rem !important;
|
||||
}
|
||||
|
||||
.emailList {
|
||||
transition:
|
||||
border-block-end 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out;
|
||||
border-block-end: 1px solid var(--mui-palette-divider);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--mui-customShadows-sm);
|
||||
border-color: transparent;
|
||||
.emailInfo:not(.show) {
|
||||
display: none !important;
|
||||
}
|
||||
.emailActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emailActions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message p:not(:last-child) {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
.message p:first-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mailReplyLayer {
|
||||
block-size: 15px;
|
||||
border-width: 1px 1px 0px;
|
||||
display: block;
|
||||
margin-inline: auto;
|
||||
border-style: solid;
|
||||
border-color: var(--mui-palette-divider);
|
||||
border-start-start-radius: var(--mui-shape-borderRadius);
|
||||
border-start-end-radius: var(--mui-shape-borderRadius);
|
||||
background-color: var(--mui-palette-background-paper);
|
||||
cursor: pointer;
|
||||
}
|
||||
.layer1 {
|
||||
inline-size: 90%;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.layer2 {
|
||||
inline-size: 95%;
|
||||
opacity: 0.7;
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Button from '@mui/material/Button'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import InputLabel from '@mui/material/InputLabel'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Switch from '@mui/material/Switch'
|
||||
|
||||
// Type Imports
|
||||
import type { Locale } from '@configs/i18n'
|
||||
|
||||
// Component Imports
|
||||
import SendInvoiceDrawer from '@views/apps/invoice/shared/SendInvoiceDrawer'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Util Imports
|
||||
import { getLocalizedUrl } from '@/utils/i18n'
|
||||
|
||||
const AddActions = () => {
|
||||
// States
|
||||
const [sendDrawerOpen, setSendDrawerOpen] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Card>
|
||||
<CardContent className='flex flex-col gap-4'>
|
||||
<Button
|
||||
fullWidth
|
||||
variant='contained'
|
||||
className='capitalize'
|
||||
startIcon={<i className='tabler-send' />}
|
||||
onClick={() => setSendDrawerOpen(true)}
|
||||
>
|
||||
Send Invoice
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
component={Link}
|
||||
color='secondary'
|
||||
variant='tonal'
|
||||
className='capitalize'
|
||||
href={getLocalizedUrl('/apps/invoice/preview/4987', locale as Locale)}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
<Button fullWidth color='secondary' variant='tonal' className='capitalize'>
|
||||
Save
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SendInvoiceDrawer open={sendDrawerOpen} handleClose={() => setSendDrawerOpen(false)} />
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CustomTextField select fullWidth defaultValue='Internet Banking' label='Accept payments via'>
|
||||
<MenuItem value='Internet Banking'>Internet Banking</MenuItem>
|
||||
<MenuItem value='Debit Card'>Debit Card</MenuItem>
|
||||
<MenuItem value='Credit Card'>Credit Card</MenuItem>
|
||||
<MenuItem value='Paypal'>Paypal</MenuItem>
|
||||
<MenuItem value='UPI Transfer'>UPI Transfer</MenuItem>
|
||||
</CustomTextField>
|
||||
<div className='flex items-center justify-between mbs-3'>
|
||||
<InputLabel htmlFor='invoice-edit-payment-terms' className='cursor-pointer'>
|
||||
Payment Terms
|
||||
</InputLabel>
|
||||
<Switch defaultChecked id='invoice-edit-payment-terms' />
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<InputLabel htmlFor='invoice-edit-client-notes' className='cursor-pointer'>
|
||||
Client Notes
|
||||
</InputLabel>
|
||||
<Switch id='invoice-edit-client-notes' />
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<InputLabel htmlFor='invoice-edit-payment-stub' className='cursor-pointer'>
|
||||
Payment Stub
|
||||
</InputLabel>
|
||||
<Switch id='invoice-edit-payment-stub' />
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddActions
|
||||
@@ -1,372 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { SyntheticEvent } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import InputAdornment from '@mui/material/InputAdornment'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Button from '@mui/material/Button'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import InputLabel from '@mui/material/InputLabel'
|
||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||
import type { Theme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { InvoiceType } from '@/types/apps/invoiceTypes'
|
||||
import type { FormDataType } from './AddCustomerDrawer'
|
||||
|
||||
// Component Imports
|
||||
import AddCustomerDrawer, { initialFormData } from './AddCustomerDrawer'
|
||||
import Logo from '@components/layout/shared/Logo'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Styled Component Imports
|
||||
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
|
||||
|
||||
const AddAction = ({ invoiceData }: { invoiceData?: InvoiceType[] }) => {
|
||||
// States
|
||||
const [open, setOpen] = useState(false)
|
||||
const [count, setCount] = useState(1)
|
||||
const [selectData, setSelectData] = useState<InvoiceType | null>(null)
|
||||
const [issuedDate, setIssuedDate] = useState<Date | null | undefined>(null)
|
||||
const [dueDate, setDueDate] = useState<Date | null | undefined>(null)
|
||||
const [formData, setFormData] = useState<FormDataType>(initialFormData)
|
||||
|
||||
// Hooks
|
||||
const isBelowMdScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'))
|
||||
const isBelowSmScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
|
||||
|
||||
const onFormSubmit = (data: FormDataType) => {
|
||||
setFormData(data)
|
||||
}
|
||||
|
||||
const deleteForm = (e: SyntheticEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
// @ts-ignore
|
||||
e.target.closest('.repeater-item').remove()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardContent className='sm:!p-12'>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<div className='p-6 bg-actionHover rounded'>
|
||||
<div className='flex justify-between gap-4 flex-col sm:flex-row'>
|
||||
<div className='flex flex-col gap-6'>
|
||||
<div className='flex items-center gap-2.5'>
|
||||
<Logo />
|
||||
</div>
|
||||
<div>
|
||||
<Typography color='text.primary'>Office 149, 450 South Brand Brooklyn</Typography>
|
||||
<Typography color='text.primary'>San Diego County, CA 91905, USA</Typography>
|
||||
<Typography color='text.primary'>+1 (123) 456 7891, +44 (876) 543 2198</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography variant='h5' className='min-is-[95px]'>
|
||||
Invoice
|
||||
</Typography>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
value={invoiceData?.[0].id}
|
||||
slotProps={{
|
||||
input: {
|
||||
disabled: true,
|
||||
startAdornment: <InputAdornment position='start'>#</InputAdornment>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<Typography className='min-is-[95px] mie-4' color='text.primary'>
|
||||
Date Issued:
|
||||
</Typography>
|
||||
<AppReactDatepicker
|
||||
boxProps={{ className: 'is-full' }}
|
||||
selected={issuedDate}
|
||||
placeholderText='YYYY-MM-DD'
|
||||
dateFormat={'yyyy-MM-dd'}
|
||||
onChange={(date: Date | null) => setIssuedDate(date)}
|
||||
customInput={<CustomTextField fullWidth />}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<Typography className='min-is-[95px] mie-4' color='text.primary'>
|
||||
Date Due:
|
||||
</Typography>
|
||||
<AppReactDatepicker
|
||||
boxProps={{ className: 'is-full' }}
|
||||
selected={dueDate}
|
||||
placeholderText='YYYY-MM-DD'
|
||||
dateFormat={'yyyy-MM-dd'}
|
||||
onChange={(date: Date | null) => setDueDate(date)}
|
||||
customInput={<CustomTextField fullWidth />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<div className='flex justify-between flex-col gap-4 flex-wrap sm:flex-row'>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
Invoice To:
|
||||
</Typography>
|
||||
<CustomTextField
|
||||
select
|
||||
className={classnames('min-is-[220px]', { 'is-1/2': isBelowSmScreen })}
|
||||
value={selectData?.id || ''}
|
||||
onChange={e => {
|
||||
setFormData({} as FormDataType)
|
||||
setSelectData(invoiceData?.slice(0, 5).filter(item => item.id === e.target.value)[0] || null)
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
className='flex items-center gap-2 !text-success !bg-transparent hover:text-success hover:!bg-[var(--mui-palette-success-lightOpacity)]'
|
||||
value=''
|
||||
onClick={() => {
|
||||
setSelectData(null)
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
<i className='tabler-plus text-base' />
|
||||
Add New Customer
|
||||
</MenuItem>
|
||||
{invoiceData?.slice(0, 5).map((invoice: InvoiceType, index) => (
|
||||
<MenuItem key={index} value={invoice.id}>
|
||||
{invoice.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</CustomTextField>
|
||||
{selectData?.id ? (
|
||||
<div>
|
||||
<Typography>{selectData?.name}</Typography>
|
||||
<Typography>{selectData?.company}</Typography>
|
||||
<Typography>{selectData?.address}</Typography>
|
||||
<Typography>{selectData?.contact}</Typography>
|
||||
<Typography>{selectData?.companyEmail}</Typography>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Typography>{formData?.name}</Typography>
|
||||
<Typography>{formData?.company}</Typography>
|
||||
<Typography>{formData?.address}</Typography>
|
||||
<Typography>{formData?.contactNumber}</Typography>
|
||||
<Typography>{formData?.email}</Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
Bill To:
|
||||
</Typography>
|
||||
<div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>Total Due:</Typography>
|
||||
<Typography>$12,110.55</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>Bank name:</Typography>
|
||||
<Typography>American Bank</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>Country:</Typography>
|
||||
<Typography>United States</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>IBAN:</Typography>
|
||||
<Typography>ETD95476213874685</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>SWIFT code:</Typography>
|
||||
<Typography>BR91905</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Divider className='border-dashed' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
{Array.from(Array(count).keys()).map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={classnames('repeater-item flex relative mbe-4 border rounded', {
|
||||
'mbs-8': !isBelowMdScreen,
|
||||
'!mbs-14': index !== 0 && !isBelowMdScreen,
|
||||
'gap-5': isBelowMdScreen
|
||||
})}
|
||||
>
|
||||
<Grid container spacing={5} className='m-0 p-5'>
|
||||
<Grid size={{ xs: 12, md: 5, lg: 6 }}>
|
||||
<Typography className='font-medium md:absolute md:-top-8' color='text.primary'>
|
||||
Item
|
||||
</Typography>
|
||||
<CustomTextField select fullWidth defaultValue='App Design' className='mbe-5'>
|
||||
<MenuItem value='App Design'>App Design</MenuItem>
|
||||
<MenuItem value='App Customization'>App Customization</MenuItem>
|
||||
<MenuItem value='ABC Template'>ABC Template</MenuItem>
|
||||
<MenuItem value='App Development'>App Development</MenuItem>
|
||||
</CustomTextField>
|
||||
<CustomTextField rows={2} fullWidth multiline defaultValue='Customization & Bug Fixes' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3, lg: 2 }}>
|
||||
<Typography className='font-medium md:absolute md:-top-8'>Cost</Typography>
|
||||
<CustomTextField
|
||||
{...(isBelowMdScreen && { fullWidth: true })}
|
||||
type='number'
|
||||
placeholder='24'
|
||||
defaultValue='24'
|
||||
className='mbe-5'
|
||||
slotProps={{
|
||||
input: {
|
||||
inputProps: { min: 0 }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className='flex flex-col'>
|
||||
<Typography component='span' color='text.primary'>
|
||||
Discount:
|
||||
</Typography>
|
||||
<div className='flex gap-2'>
|
||||
<Typography component='span' color='text.primary'>
|
||||
0%
|
||||
</Typography>
|
||||
<Tooltip title='Tax 1' placement='top'>
|
||||
<Typography component='span' color='text.primary'>
|
||||
0%
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Tooltip title='Tax 2' placement='top'>
|
||||
<Typography component='span' color='text.primary'>
|
||||
0%
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 2 }}>
|
||||
<Typography className='font-medium md:absolute md:-top-8'>Hours</Typography>
|
||||
<CustomTextField
|
||||
{...(isBelowMdScreen && { fullWidth: true })}
|
||||
type='number'
|
||||
placeholder='1'
|
||||
defaultValue='1'
|
||||
slotProps={{
|
||||
input: {
|
||||
inputProps: { min: 0 }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 2 }}>
|
||||
<Typography className='font-medium md:absolute md:-top-8'>Price</Typography>
|
||||
<Typography>$24.00</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<div className='flex flex-col justify-start border-is'>
|
||||
<IconButton size='small' onClick={deleteForm}>
|
||||
<i className='tabler-x text-2xl text-actionActive' />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Button
|
||||
size='small'
|
||||
variant='contained'
|
||||
onClick={() => setCount(count + 1)}
|
||||
startIcon={<i className='tabler-plus' />}
|
||||
>
|
||||
Add Item
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Divider className='border-dashed' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<div className='flex justify-between flex-col gap-4 sm:flex-row'>
|
||||
<div className='flex flex-col gap-4 order-2 sm:order-[unset]'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
Salesperson:
|
||||
</Typography>
|
||||
<CustomTextField defaultValue='Tommy Shelby' />
|
||||
</div>
|
||||
<CustomTextField placeholder='Thanks for your business' />
|
||||
</div>
|
||||
<div className='min-is-[200px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography>Subtotal:</Typography>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
$1800
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography>Discount:</Typography>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
$28
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography>Tax:</Typography>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
21%
|
||||
</Typography>
|
||||
</div>
|
||||
<Divider className='mlb-2' />
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography>Total:</Typography>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
$1690
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Divider className='border-dashed' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<InputLabel htmlFor='invoice-note' className='inline-flex mbe-1 text-textPrimary'>
|
||||
Note:
|
||||
</InputLabel>
|
||||
<CustomTextField
|
||||
id='invoice-note'
|
||||
rows={2}
|
||||
fullWidth
|
||||
multiline
|
||||
className='border rounded'
|
||||
defaultValue='It was a pleasure working with you and your team. We hope you will keep us in mind for future freelance
|
||||
projects. Thank You!'
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AddCustomerDrawer open={open} setOpen={setOpen} onFormSubmit={onFormSubmit} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddAction
|
||||
@@ -1,144 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { FormEvent } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Button from '@mui/material/Button'
|
||||
import Divider from '@mui/material/Divider'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
onFormSubmit: (formData: FormDataType) => void
|
||||
}
|
||||
|
||||
export type FormDataType = {
|
||||
name: string
|
||||
company: string
|
||||
email: string
|
||||
address: string
|
||||
country: string
|
||||
contactNumber: string
|
||||
}
|
||||
|
||||
// Vars
|
||||
export const initialFormData: FormDataType = {
|
||||
name: '',
|
||||
company: '',
|
||||
email: '',
|
||||
address: '',
|
||||
country: 'USA',
|
||||
contactNumber: ''
|
||||
}
|
||||
|
||||
const countries = ['USA', 'UK', 'Russia', 'Australia', 'Canada']
|
||||
|
||||
const AddCustomerDrawer = ({ open, setOpen, onFormSubmit }: Props) => {
|
||||
// States
|
||||
const [data, setData] = useState<FormDataType>(initialFormData)
|
||||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setOpen(false)
|
||||
onFormSubmit(data)
|
||||
handleReset()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setOpen(false)
|
||||
setData(initialFormData)
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
anchor='right'
|
||||
variant='temporary'
|
||||
onClose={handleReset}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
|
||||
>
|
||||
<div className='flex items-center justify-between plb-5 pli-6'>
|
||||
<Typography variant='h5'>Add New Customer</Typography>
|
||||
<IconButton size='small' onClick={handleReset}>
|
||||
<i className='tabler-x text-2xl text-textPrimary' />
|
||||
</IconButton>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className='p-6'>
|
||||
<form onSubmit={e => handleSubmit(e)} className='flex flex-col gap-5'>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
id='name'
|
||||
label='Name'
|
||||
value={data.name}
|
||||
onChange={e => setData({ ...data, name: e.target.value })}
|
||||
/>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
id='company'
|
||||
label='Company'
|
||||
value={data.company}
|
||||
onChange={e => setData({ ...data, company: e.target.value })}
|
||||
/>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
id='email'
|
||||
label='Email'
|
||||
value={data.email}
|
||||
onChange={e => setData({ ...data, email: e.target.value })}
|
||||
/>
|
||||
<CustomTextField
|
||||
rows={6}
|
||||
multiline
|
||||
fullWidth
|
||||
id='address'
|
||||
label='Address'
|
||||
value={data.address}
|
||||
onChange={e => setData({ ...data, address: e.target.value })}
|
||||
/>
|
||||
<CustomTextField
|
||||
select
|
||||
id='country'
|
||||
label='Country'
|
||||
name='country'
|
||||
variant='outlined'
|
||||
value={data?.country?.toLowerCase().replace(/\s+/g, '-') || ''}
|
||||
onChange={e => setData({ ...data, country: e.target.value })}
|
||||
>
|
||||
{countries.map((item, index) => (
|
||||
<MenuItem key={index} value={item.toLowerCase().replace(/\s+/g, '-')}>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</CustomTextField>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
id='contact'
|
||||
type='number'
|
||||
label='Contact Number'
|
||||
value={data.contactNumber}
|
||||
onChange={e => setData({ ...data, contactNumber: e.target.value })}
|
||||
/>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Button variant='contained' type='submit'>
|
||||
Add
|
||||
</Button>
|
||||
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddCustomerDrawer
|
||||
@@ -1,114 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Button from '@mui/material/Button'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import InputLabel from '@mui/material/InputLabel'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Switch from '@mui/material/Switch'
|
||||
|
||||
// Type Imports
|
||||
import type { Locale } from '@configs/i18n'
|
||||
|
||||
// Component Imports
|
||||
import AddPaymentDrawer from '@views/apps/invoice/shared/AddPaymentDrawer'
|
||||
import SendInvoiceDrawer from '@views/apps/invoice/shared/SendInvoiceDrawer'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Util Imports
|
||||
import { getLocalizedUrl } from '@/utils/i18n'
|
||||
|
||||
const EditActions = ({ id }: { id: string }) => {
|
||||
// States
|
||||
const [paymentDrawerOpen, setPaymentDrawerOpen] = useState(false)
|
||||
const [sendDrawerOpen, setSendDrawerOpen] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Card>
|
||||
<CardContent className='flex flex-col gap-4'>
|
||||
<Button
|
||||
fullWidth
|
||||
variant='contained'
|
||||
className='capitalize'
|
||||
startIcon={<i className='tabler-send' />}
|
||||
onClick={() => setSendDrawerOpen(true)}
|
||||
>
|
||||
Send Invoice
|
||||
</Button>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Button
|
||||
fullWidth
|
||||
component={Link}
|
||||
color='secondary'
|
||||
variant='tonal'
|
||||
className='capitalize'
|
||||
href={getLocalizedUrl(`/apps/invoice/preview/${id}`, locale as Locale)}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
<Button fullWidth color='secondary' variant='tonal' className='capitalize'>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
fullWidth
|
||||
color='success'
|
||||
variant='contained'
|
||||
className='capitalize'
|
||||
onClick={() => setPaymentDrawerOpen(true)}
|
||||
startIcon={<i className='tabler-currency-dollar' />}
|
||||
>
|
||||
Add Payment
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AddPaymentDrawer open={paymentDrawerOpen} handleClose={() => setPaymentDrawerOpen(false)} />
|
||||
<SendInvoiceDrawer open={sendDrawerOpen} handleClose={() => setSendDrawerOpen(false)} />
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CustomTextField select fullWidth defaultValue='Internet Banking' label='Accept payments via'>
|
||||
<MenuItem value='Internet Banking'>Internet Banking</MenuItem>
|
||||
<MenuItem value='Debit Card'>Debit Card</MenuItem>
|
||||
<MenuItem value='Credit Card'>Credit Card</MenuItem>
|
||||
<MenuItem value='Paypal'>Paypal</MenuItem>
|
||||
<MenuItem value='UPI Transfer'>UPI Transfer</MenuItem>
|
||||
</CustomTextField>
|
||||
<div className='flex items-center justify-between gap-6 mbs-3'>
|
||||
<InputLabel htmlFor='invoice-edit-payment-terms' className='cursor-pointer'>
|
||||
Payment Terms
|
||||
</InputLabel>
|
||||
<Switch defaultChecked id='invoice-edit-payment-terms' />
|
||||
</div>
|
||||
<div className='flex items-center justify-between gap-6'>
|
||||
<InputLabel htmlFor='invoice-edit-client-notes' className='cursor-pointer'>
|
||||
Client Notes
|
||||
</InputLabel>
|
||||
<Switch id='invoice-edit-client-notes' />
|
||||
</div>
|
||||
<div className='flex items-center justify-between gap-6'>
|
||||
<InputLabel htmlFor='invoice-edit-payment-stub' className='cursor-pointer'>
|
||||
Payment Stub
|
||||
</InputLabel>
|
||||
<Switch id='invoice-edit-payment-stub' />
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditActions
|
||||
@@ -1,342 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { SyntheticEvent } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import InputAdornment from '@mui/material/InputAdornment'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Button from '@mui/material/Button'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import InputLabel from '@mui/material/InputLabel'
|
||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||
import type { Theme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { InvoiceType } from '@/types/apps/invoiceTypes'
|
||||
|
||||
// Component Imports
|
||||
import Logo from '@components/layout/shared/Logo'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Styled Component Imports
|
||||
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
|
||||
|
||||
const EditCard = ({ invoiceData, id, data }: { invoiceData?: InvoiceType; id: string; data?: InvoiceType[] }) => {
|
||||
// States
|
||||
const [selectData, setSelectData] = useState<InvoiceType | null>(data?.[0] || null)
|
||||
const [count, setCount] = useState(1)
|
||||
const [issueDate, setIssueDate] = useState(new Date(invoiceData?.issuedDate ?? ''))
|
||||
const [dueDate, setDueDate] = useState(new Date(invoiceData?.dueDate ?? ''))
|
||||
|
||||
// Hooks
|
||||
const isBelowMdScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'))
|
||||
|
||||
const deleteForm = (e: SyntheticEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
// @ts-ignore
|
||||
e.target.closest('.repeater-item').remove()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardContent className='sm:!p-12'>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<div className='p-6 rounded bg-actionHover'>
|
||||
<div className='flex justify-between gap-4 flex-col sm:flex-row'>
|
||||
<div className='flex flex-col gap-6'>
|
||||
<div className='flex items-center gap-2.5'>
|
||||
<Logo />
|
||||
</div>
|
||||
<div>
|
||||
<Typography color='text.primary'>Office 149, 450 South Brand Brooklyn</Typography>
|
||||
<Typography color='text.primary'>San Diego County, CA 91905, USA</Typography>
|
||||
<Typography color='text.primary'>+1 (123) 456 7891, +44 (876) 543 2198</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography variant='h5' className='min-is-[95px]'>
|
||||
Invoice
|
||||
</Typography>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
value={id}
|
||||
slotProps={{
|
||||
input: {
|
||||
disabled: true,
|
||||
startAdornment: <InputAdornment position='start'>#</InputAdornment>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<Typography className='min-is-[95px] mie-4' color='text.primary'>
|
||||
Date Issued:
|
||||
</Typography>
|
||||
<AppReactDatepicker
|
||||
boxProps={{ className: 'is-full' }}
|
||||
selected={issueDate}
|
||||
id='payment-date'
|
||||
onChange={(date: Date | null) => date !== null && setIssueDate(date)}
|
||||
customInput={<CustomTextField fullWidth />}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<Typography className='min-is-[95px] mie-4' color='text.primary'>
|
||||
Date Due:
|
||||
</Typography>
|
||||
<AppReactDatepicker
|
||||
boxProps={{ className: 'is-full' }}
|
||||
selected={dueDate}
|
||||
id='payment-date'
|
||||
onChange={(date: Date | null) => date !== null && setDueDate(date)}
|
||||
customInput={<CustomTextField fullWidth />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<div className='flex justify-between flex-col gap-4 flex-wrap sm:flex-row'>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
Invoice To:
|
||||
</Typography>
|
||||
<CustomTextField
|
||||
select
|
||||
className='is-1/2 min-is-[220px] sm:is-auto'
|
||||
value={selectData?.id}
|
||||
onChange={e => {
|
||||
setSelectData(data?.slice(0, 5).filter(item => item.id === e.target.value)[0] || null)
|
||||
}}
|
||||
>
|
||||
{data?.slice(0, 5).map((invoice: InvoiceType, index) => (
|
||||
<MenuItem key={index} value={invoice.id}>
|
||||
{invoice.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</CustomTextField>
|
||||
<div>
|
||||
<Typography>{selectData?.name}</Typography>
|
||||
<Typography>{selectData?.company}</Typography>
|
||||
<Typography>{selectData?.address}</Typography>
|
||||
<Typography>{selectData?.contact}</Typography>
|
||||
<Typography>{selectData?.companyEmail}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
Bill To:
|
||||
</Typography>
|
||||
<div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>Total Due:</Typography>
|
||||
<Typography>$12,110.55</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>Bank name:</Typography>
|
||||
<Typography>American Bank</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>Country:</Typography>
|
||||
<Typography>United States</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>IBAN:</Typography>
|
||||
<Typography>ETD95476213874685</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>SWIFT code:</Typography>
|
||||
<Typography>BR91905</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Divider className='border-dashed' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
{Array.from(Array(count).keys()).map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={classnames('repeater-item flex relative mbe-4 border rounded', {
|
||||
'mbs-8': !isBelowMdScreen,
|
||||
'!mbs-14': index !== 0 && !isBelowMdScreen,
|
||||
'gap-5': isBelowMdScreen
|
||||
})}
|
||||
>
|
||||
<Grid container spacing={5} className='m-0 p-5'>
|
||||
<Grid size={{ xs: 12, md: 5, lg: 6 }}>
|
||||
<Typography className='font-medium md:absolute md:-top-8' color='text.primary'>
|
||||
Item
|
||||
</Typography>
|
||||
<CustomTextField select fullWidth defaultValue='App Design' className='mbe-5'>
|
||||
<MenuItem value='App Design'>App Design</MenuItem>
|
||||
<MenuItem value='App Customization'>App Customization</MenuItem>
|
||||
<MenuItem value='ABC Template'>ABC Template</MenuItem>
|
||||
<MenuItem value='App Development'>App Development</MenuItem>
|
||||
</CustomTextField>
|
||||
<CustomTextField rows={2} fullWidth multiline defaultValue='Customization & Bug Fixes' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3, lg: 2 }}>
|
||||
<Typography className='font-medium md:absolute md:-top-8' color='text.primary'>
|
||||
Cost
|
||||
</Typography>
|
||||
<CustomTextField
|
||||
{...(isBelowMdScreen && { fullWidth: true })}
|
||||
type='number'
|
||||
placeholder='24'
|
||||
defaultValue='24'
|
||||
className='mbe-5'
|
||||
slotProps={{
|
||||
input: {
|
||||
inputProps: { min: 0 }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className='flex flex-col'>
|
||||
<Typography component='span' color='text.primary'>
|
||||
Discount:
|
||||
</Typography>
|
||||
<div className='flex gap-2'>
|
||||
<Typography component='span' color='text.primary'>
|
||||
0%
|
||||
</Typography>
|
||||
<Tooltip title='Tax 1' placement='top'>
|
||||
<Typography component='span' color='text.primary'>
|
||||
0%
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Tooltip title='Tax 2' placement='top'>
|
||||
<Typography component='span' color='text.primary'>
|
||||
0%
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 2 }}>
|
||||
<Typography className='font-medium md:absolute md:-top-8' color='text.primary'>
|
||||
Hours
|
||||
</Typography>
|
||||
<CustomTextField
|
||||
{...(isBelowMdScreen && { fullWidth: true })}
|
||||
type='number'
|
||||
placeholder='1'
|
||||
defaultValue='1'
|
||||
slotProps={{
|
||||
input: {
|
||||
inputProps: { min: 0 }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 2 }}>
|
||||
<Typography className='font-medium md:absolute md:-top-8' color='text.primary'>
|
||||
Price
|
||||
</Typography>
|
||||
<Typography>$24.00</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<div className='flex flex-col justify-start border-is'>
|
||||
<IconButton size='small' onClick={deleteForm}>
|
||||
<i className='tabler-x text-2xl text-actionActive' />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Button
|
||||
size='small'
|
||||
variant='contained'
|
||||
onClick={() => setCount(count + 1)}
|
||||
startIcon={<i className='tabler-plus' />}
|
||||
>
|
||||
Add Item
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Divider className='border-dashed' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<div className='flex justify-between flex-col gap-4 sm:flex-row'>
|
||||
<div className='flex flex-col gap-4 order-2 sm:order-[unset]'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
Salesperson:
|
||||
</Typography>
|
||||
<CustomTextField defaultValue='Tommy Shelby' />
|
||||
</div>
|
||||
<CustomTextField defaultValue='Thanks for your business' />
|
||||
</div>
|
||||
<div className='min-is-[200px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography>Subtotal:</Typography>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
$1800
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography>Discount:</Typography>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
$28
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography>Tax:</Typography>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
21%
|
||||
</Typography>
|
||||
</div>
|
||||
<Divider className='mlb-2' />
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography>Total:</Typography>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
$1690
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Divider className='border-dashed' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<InputLabel htmlFor='invoice-note' className='inline-flex mbe-1 text-textPrimary'>
|
||||
Note:
|
||||
</InputLabel>
|
||||
<CustomTextField
|
||||
id='invoice-note'
|
||||
rows={2}
|
||||
fullWidth
|
||||
multiline
|
||||
className='border rounded'
|
||||
defaultValue='It was a pleasure working with you and your team. We hope you will keep us in mind for future freelance
|
||||
projects. Thank You!'
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditCard
|
||||
@@ -1,84 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import Avatar from '@mui/material/Avatar'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||
import type { Theme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Vars
|
||||
const data = [
|
||||
{
|
||||
title: 24,
|
||||
subtitle: 'Clients',
|
||||
icon: 'tabler-user'
|
||||
},
|
||||
{
|
||||
title: 165,
|
||||
subtitle: 'Invoices',
|
||||
icon: 'tabler-file-invoice'
|
||||
},
|
||||
{
|
||||
title: '$2.46k',
|
||||
subtitle: 'Paid',
|
||||
icon: 'tabler-checks'
|
||||
},
|
||||
{
|
||||
title: '$876',
|
||||
subtitle: 'Unpaid',
|
||||
icon: 'tabler-circle-off'
|
||||
}
|
||||
]
|
||||
|
||||
const InvoiceCard = () => {
|
||||
// Hooks
|
||||
const isBelowMdScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'))
|
||||
const isBelowSmScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Grid container spacing={6}>
|
||||
{data.map((item, index) => (
|
||||
<Grid
|
||||
size={{ xs: 12, sm: 6, md: 3 }}
|
||||
key={index}
|
||||
className={classnames({
|
||||
'[&:nth-of-type(odd)>div]:pie-6 [&:nth-of-type(odd)>div]:border-ie':
|
||||
isBelowMdScreen && !isBelowSmScreen,
|
||||
'[&:not(:last-child)>div]:pie-6 [&:not(:last-child)>div]:border-ie': !isBelowMdScreen
|
||||
})}
|
||||
>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex flex-col'>
|
||||
<Typography variant='h4'>{item.title}</Typography>
|
||||
<Typography>{item.subtitle}</Typography>
|
||||
</div>
|
||||
<Avatar variant='rounded' className='is-[42px] bs-[42px]'>
|
||||
<i className={classnames(item.icon, 'text-[26px]')} />
|
||||
</Avatar>
|
||||
</div>
|
||||
{isBelowMdScreen && !isBelowSmScreen && index < data.length - 2 && (
|
||||
<Divider
|
||||
className={classnames('mbs-6', {
|
||||
'mie-6': index % 2 === 0
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{isBelowSmScreen && index < data.length - 1 && <Divider className='mbs-6' />}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default InvoiceCard
|
||||
@@ -1,461 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
|
||||
// MUI Imports
|
||||
import Button from '@mui/material/Button'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import type { TextFieldProps } from '@mui/material/TextField'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Third-party Imports
|
||||
import type { RankingInfo } from '@tanstack/match-sorter-utils'
|
||||
import { rankItem } from '@tanstack/match-sorter-utils'
|
||||
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedMinMaxValues,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable
|
||||
} from '@tanstack/react-table'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { InvoiceType } from '@/types/apps/invoiceTypes'
|
||||
import type { Locale } from '@configs/i18n'
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
// Util Imports
|
||||
import { getInitials } from '@/utils/getInitials'
|
||||
import { getLocalizedUrl } from '@/utils/i18n'
|
||||
|
||||
// Style Imports
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface FilterFns {
|
||||
fuzzy: FilterFn<unknown>
|
||||
}
|
||||
interface FilterMeta {
|
||||
itemRank: RankingInfo
|
||||
}
|
||||
}
|
||||
|
||||
type InvoiceTypeWithAction = InvoiceType & {
|
||||
action?: string
|
||||
}
|
||||
|
||||
type InvoiceStatusObj = {
|
||||
[key: string]: {
|
||||
icon: string
|
||||
color: ThemeColor
|
||||
}
|
||||
}
|
||||
|
||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value)
|
||||
|
||||
// Store the itemRank info
|
||||
addMeta({
|
||||
itemRank
|
||||
})
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed
|
||||
}
|
||||
|
||||
const DebouncedInput = ({
|
||||
value: initialValue,
|
||||
onChange,
|
||||
debounce = 500,
|
||||
...props
|
||||
}: {
|
||||
value: string | number
|
||||
onChange: (value: string | number) => void
|
||||
debounce?: number
|
||||
} & Omit<TextFieldProps, 'onChange'>) => {
|
||||
// States
|
||||
const [value, setValue] = useState(initialValue)
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue)
|
||||
}, [initialValue])
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
onChange(value)
|
||||
}, debounce)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value])
|
||||
|
||||
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
|
||||
}
|
||||
|
||||
// Vars
|
||||
const invoiceStatusObj: InvoiceStatusObj = {
|
||||
Sent: { color: 'secondary', icon: 'tabler-send-2' },
|
||||
Paid: { color: 'success', icon: 'tabler-check' },
|
||||
Draft: { color: 'primary', icon: 'tabler-mail' },
|
||||
'Partial Payment': { color: 'warning', icon: 'tabler-chart-pie-2' },
|
||||
'Past Due': { color: 'error', icon: 'tabler-alert-circle' },
|
||||
Downloaded: { color: 'info', icon: 'tabler-arrow-down' }
|
||||
}
|
||||
|
||||
// Column Definitions
|
||||
const columnHelper = createColumnHelper<InvoiceTypeWithAction>()
|
||||
|
||||
const InvoiceListTable = ({ invoiceData }: { invoiceData?: InvoiceType[] }) => {
|
||||
// States
|
||||
const [status, setStatus] = useState<InvoiceType['invoiceStatus']>('')
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
const [data, setData] = useState(...[invoiceData])
|
||||
const [filteredData, setFilteredData] = useState(data)
|
||||
const [globalFilter, setGlobalFilter] = useState('')
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
const columns = useMemo<ColumnDef<InvoiceTypeWithAction, any>[]>(
|
||||
() => [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
{...{
|
||||
checked: table.getIsAllRowsSelected(),
|
||||
indeterminate: table.getIsSomeRowsSelected(),
|
||||
onChange: table.getToggleAllRowsSelectedHandler()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
{...{
|
||||
checked: row.getIsSelected(),
|
||||
disabled: !row.getCanSelect(),
|
||||
indeterminate: row.getIsSomeSelected(),
|
||||
onChange: row.getToggleSelectedHandler()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
columnHelper.accessor('id', {
|
||||
header: '#',
|
||||
cell: ({ row }) => (
|
||||
<Typography
|
||||
component={Link}
|
||||
href={getLocalizedUrl(`/apps/invoice/preview/${row.original.id}`, locale as Locale)}
|
||||
color='primary.main'
|
||||
>{`#${row.original.id}`}</Typography>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('invoiceStatus', {
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
<Typography variant='body2' component='span' className='text-inherit'>
|
||||
{row.original.invoiceStatus}
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography variant='body2' component='span' className='text-inherit'>
|
||||
Balance:
|
||||
</Typography>{' '}
|
||||
{row.original.balance}
|
||||
<br />
|
||||
<Typography variant='body2' component='span' className='text-inherit'>
|
||||
Due Date:
|
||||
</Typography>{' '}
|
||||
{row.original.dueDate}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CustomAvatar skin='light' color={invoiceStatusObj[row.original.invoiceStatus].color} size={28}>
|
||||
<i className={classnames('bs-4 is-4', invoiceStatusObj[row.original.invoiceStatus].icon)} />
|
||||
</CustomAvatar>
|
||||
</Tooltip>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('name', {
|
||||
header: 'Client',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center gap-3'>
|
||||
{getAvatar({ avatar: row.original.avatar, name: row.original.name })}
|
||||
<div className='flex flex-col'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
{row.original.name}
|
||||
</Typography>
|
||||
<Typography variant='body2'>{row.original.companyEmail}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('total', {
|
||||
header: 'Total',
|
||||
cell: ({ row }) => <Typography>{`$${row.original.total}`}</Typography>
|
||||
}),
|
||||
columnHelper.accessor('issuedDate', {
|
||||
header: 'Issued Date',
|
||||
cell: ({ row }) => <Typography>{row.original.issuedDate}</Typography>
|
||||
}),
|
||||
columnHelper.accessor('balance', {
|
||||
header: 'Balance',
|
||||
cell: ({ row }) => {
|
||||
return row.original.balance === 0 ? (
|
||||
<Chip label='Paid' color='success' size='small' variant='tonal' />
|
||||
) : (
|
||||
<Typography color='text.primary'>{row.original.balance}</Typography>
|
||||
)
|
||||
}
|
||||
}),
|
||||
columnHelper.accessor('action', {
|
||||
header: 'Action',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center'>
|
||||
<IconButton onClick={() => setData(data?.filter(invoice => invoice.id !== row.original.id))}>
|
||||
<i className='tabler-trash text-textSecondary' />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<Link
|
||||
href={getLocalizedUrl(`/apps/invoice/preview/${row.original.id}`, locale as Locale)}
|
||||
className='flex'
|
||||
>
|
||||
<i className='tabler-eye text-textSecondary' />
|
||||
</Link>
|
||||
</IconButton>
|
||||
<OptionMenu
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
iconClassName='text-textSecondary'
|
||||
options={[
|
||||
{
|
||||
text: 'Download',
|
||||
icon: 'tabler-download',
|
||||
menuItemProps: { className: 'flex items-center gap-2 text-textSecondary' }
|
||||
},
|
||||
{
|
||||
text: 'Edit',
|
||||
icon: 'tabler-pencil',
|
||||
href: getLocalizedUrl(`/apps/invoice/edit/${row.original.id}`, locale as Locale),
|
||||
linkProps: {
|
||||
className: 'flex items-center is-full plb-2 pli-4 gap-2 text-textSecondary'
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Duplicate',
|
||||
icon: 'tabler-copy',
|
||||
menuItemProps: { className: 'flex items-center gap-2 text-textSecondary' }
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false
|
||||
})
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[data, filteredData]
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredData as InvoiceType[],
|
||||
columns,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter
|
||||
},
|
||||
state: {
|
||||
rowSelection,
|
||||
globalFilter
|
||||
},
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: 10
|
||||
}
|
||||
},
|
||||
enableRowSelection: true, //enable row selection for all rows
|
||||
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
|
||||
globalFilterFn: fuzzyFilter,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
getFacetedMinMaxValues: getFacetedMinMaxValues()
|
||||
})
|
||||
|
||||
const getAvatar = (params: Pick<InvoiceType, 'avatar' | 'name'>) => {
|
||||
const { avatar, name } = params
|
||||
|
||||
if (avatar) {
|
||||
return <CustomAvatar src={avatar} skin='light' size={34} />
|
||||
} else {
|
||||
return (
|
||||
<CustomAvatar skin='light' size={34}>
|
||||
{getInitials(name as string)}
|
||||
</CustomAvatar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const filteredData = data?.filter(invoice => {
|
||||
if (status && invoice.invoiceStatus.toLowerCase().replace(/\s+/g, '-') !== status) return false
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
setFilteredData(filteredData)
|
||||
}, [status, data, setFilteredData])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className='flex justify-between flex-col items-start md:items-center md:flex-row gap-4'>
|
||||
<div className='flex flex-col sm:flex-row items-center justify-between gap-4 is-full sm:is-auto'>
|
||||
<div className='flex items-center gap-2 is-full sm:is-auto'>
|
||||
<Typography className='hidden sm:block'>Show</Typography>
|
||||
<CustomTextField
|
||||
select
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={e => table.setPageSize(Number(e.target.value))}
|
||||
className='is-[70px] max-sm:is-full'
|
||||
>
|
||||
<MenuItem value='10'>10</MenuItem>
|
||||
<MenuItem value='25'>25</MenuItem>
|
||||
<MenuItem value='50'>50</MenuItem>
|
||||
</CustomTextField>
|
||||
</div>
|
||||
<Button
|
||||
variant='contained'
|
||||
component={Link}
|
||||
startIcon={<i className='tabler-plus' />}
|
||||
href={getLocalizedUrl('apps/invoice/add', locale as Locale)}
|
||||
className='max-sm:is-full'
|
||||
>
|
||||
Create Invoice
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex max-sm:flex-col max-sm:is-full sm:items-center gap-4'>
|
||||
<DebouncedInput
|
||||
value={globalFilter ?? ''}
|
||||
onChange={value => setGlobalFilter(String(value))}
|
||||
placeholder='Search Invoice'
|
||||
className='max-sm:is-full sm:is-[250px]'
|
||||
/>
|
||||
<CustomTextField
|
||||
select
|
||||
id='select-status'
|
||||
value={status}
|
||||
onChange={e => setStatus(e.target.value)}
|
||||
className='max-sm:is-full sm:is-[160px]'
|
||||
slotProps={{
|
||||
select: { displayEmpty: true }
|
||||
}}
|
||||
>
|
||||
<MenuItem value=''>Invoice Status</MenuItem>
|
||||
<MenuItem value='downloaded'>Downloaded</MenuItem>
|
||||
<MenuItem value='draft'>Draft</MenuItem>
|
||||
<MenuItem value='paid'>Paid</MenuItem>
|
||||
<MenuItem value='partial-payment'>Partial Payment</MenuItem>
|
||||
<MenuItem value='past-due'>Past Due</MenuItem>
|
||||
<MenuItem value='sent'>Sent</MenuItem>
|
||||
</CustomTextField>
|
||||
</div>
|
||||
</CardContent>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className={tableStyles.table}>
|
||||
<thead>
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<th key={header.id}>
|
||||
{header.isPlaceholder ? null : (
|
||||
<>
|
||||
<div
|
||||
className={classnames({
|
||||
'flex items-center': header.column.getIsSorted(),
|
||||
'cursor-pointer select-none': header.column.getCanSort()
|
||||
})}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{{
|
||||
asc: <i className='tabler-chevron-up text-xl' />,
|
||||
desc: <i className='tabler-chevron-down text-xl' />
|
||||
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
{table.getFilteredRowModel().rows.length === 0 ? (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
||||
No data available
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
) : (
|
||||
<tbody>
|
||||
{table
|
||||
.getRowModel()
|
||||
.rows.slice(0, table.getState().pagination.pageSize)
|
||||
.map(row => {
|
||||
return (
|
||||
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
{/* <TablePagination
|
||||
component={() => <TablePaginationComponent table={table} />}
|
||||
count={table.getFilteredRowModel().rows.length}
|
||||
rowsPerPage={table.getState().pagination.pageSize}
|
||||
page={table.getState().pagination.pageIndex}
|
||||
onPageChange={(_, page) => {
|
||||
table.setPageIndex(page)
|
||||
}}
|
||||
onRowsPerPageChange={e => table.setPageSize(Number(e.target.value))}
|
||||
/> */}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default InvoiceListTable
|
||||
@@ -1,24 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Type Imports
|
||||
import type { InvoiceType } from '@/types/apps/invoiceTypes'
|
||||
|
||||
// Component Imports
|
||||
import InvoiceListTable from './InvoiceListTable'
|
||||
import InvoiceCard from './InvoiceCard'
|
||||
|
||||
const InvoiceList = ({ invoiceData }: { invoiceData?: InvoiceType[] }) => {
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<InvoiceCard />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<InvoiceListTable invoiceData={invoiceData} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default InvoiceList
|
||||
@@ -1,80 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Button from '@mui/material/Button'
|
||||
|
||||
// Type Imports
|
||||
import type { Locale } from '@configs/i18n'
|
||||
|
||||
// Component Imports
|
||||
import AddPaymentDrawer from '@views/apps/invoice/shared/AddPaymentDrawer'
|
||||
import SendInvoiceDrawer from '@views/apps/invoice/shared/SendInvoiceDrawer'
|
||||
|
||||
// Util Imports
|
||||
import { getLocalizedUrl } from '@/utils/i18n'
|
||||
|
||||
const PreviewActions = ({ id, onButtonClick }: { id: string; onButtonClick: () => void }) => {
|
||||
// States
|
||||
const [paymentDrawerOpen, setPaymentDrawerOpen] = useState(false)
|
||||
const [sendDrawerOpen, setSendDrawerOpen] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardContent className='flex flex-col gap-4'>
|
||||
<Button
|
||||
fullWidth
|
||||
variant='contained'
|
||||
className='capitalize'
|
||||
startIcon={<i className='tabler-send' />}
|
||||
onClick={() => setSendDrawerOpen(true)}
|
||||
>
|
||||
Send Invoice
|
||||
</Button>
|
||||
<Button fullWidth color='secondary' variant='tonal' className='capitalize'>
|
||||
Download
|
||||
</Button>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Button fullWidth color='secondary' variant='tonal' className='capitalize' onClick={onButtonClick}>
|
||||
Print
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
component={Link}
|
||||
color='secondary'
|
||||
variant='tonal'
|
||||
className='capitalize'
|
||||
href={getLocalizedUrl(`/apps/invoice/edit/${id}`, locale as Locale)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
fullWidth
|
||||
color='success'
|
||||
variant='contained'
|
||||
className='capitalize'
|
||||
onClick={() => setPaymentDrawerOpen(true)}
|
||||
startIcon={<i className='tabler-currency-dollar' />}
|
||||
>
|
||||
Add Payment
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AddPaymentDrawer open={paymentDrawerOpen} handleClose={() => setPaymentDrawerOpen(false)} />
|
||||
<SendInvoiceDrawer open={sendDrawerOpen} handleClose={() => setSendDrawerOpen(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PreviewActions
|
||||
@@ -1,219 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Divider from '@mui/material/Divider'
|
||||
|
||||
// Type Imports
|
||||
import type { InvoiceType } from '@/types/apps/invoiceTypes'
|
||||
|
||||
// Component Imports
|
||||
import Logo from '@components/layout/shared/Logo'
|
||||
|
||||
// Style Imports
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
import './print.css'
|
||||
|
||||
// Vars
|
||||
const data = [
|
||||
{
|
||||
Item: 'Premium Branding Package',
|
||||
Description: 'Branding & Promotion',
|
||||
Hours: 48,
|
||||
Qty: 1,
|
||||
Total: '$32'
|
||||
},
|
||||
{
|
||||
Item: 'Social Media',
|
||||
Description: 'Social media templates',
|
||||
Hours: 42,
|
||||
Qty: 1,
|
||||
Total: '$28'
|
||||
},
|
||||
{
|
||||
Item: 'Web Design',
|
||||
Description: 'Web designing package',
|
||||
Hours: 46,
|
||||
Qty: 1,
|
||||
Total: '$24'
|
||||
},
|
||||
{
|
||||
Item: 'SEO',
|
||||
Description: 'Search engine optimization',
|
||||
Hours: 40,
|
||||
Qty: 1,
|
||||
Total: '$22'
|
||||
}
|
||||
]
|
||||
|
||||
const PreviewCard = ({ invoiceData, id }: { invoiceData?: InvoiceType; id: string }) => {
|
||||
return (
|
||||
<Card className='previewCard'>
|
||||
<CardContent className='sm:!p-12'>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<div className='p-6 bg-actionHover rounded'>
|
||||
<div className='flex justify-between gap-y-4 flex-col sm:flex-row'>
|
||||
<div className='flex flex-col gap-6'>
|
||||
<div className='flex items-center gap-2.5'>
|
||||
<Logo />
|
||||
</div>
|
||||
<div>
|
||||
<Typography color='text.primary'>Office 149, 450 South Brand Brooklyn</Typography>
|
||||
<Typography color='text.primary'>San Diego County, CA 91905, USA</Typography>
|
||||
<Typography color='text.primary'>+1 (123) 456 7891, +44 (876) 543 2198</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-6'>
|
||||
<Typography variant='h5'>{`Invoice #${id}`}</Typography>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography color='text.primary'>{`Date Issued: ${invoiceData?.issuedDate}`}</Typography>
|
||||
<Typography color='text.primary'>{`Date Due: ${invoiceData?.dueDate}`}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
Invoice To:
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography>{invoiceData?.name}</Typography>
|
||||
<Typography>{invoiceData?.company}</Typography>
|
||||
<Typography>{invoiceData?.address}</Typography>
|
||||
<Typography>{invoiceData?.contact}</Typography>
|
||||
<Typography>{invoiceData?.companyEmail}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
Bill To:
|
||||
</Typography>
|
||||
<div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>Total Due:</Typography>
|
||||
<Typography>$12,110.55</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>Bank name:</Typography>
|
||||
<Typography>American Bank</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>Country:</Typography>
|
||||
<Typography>United States</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>IBAN:</Typography>
|
||||
<Typography>ETD95476213874685</Typography>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='min-is-[100px]'>SWIFT code:</Typography>
|
||||
<Typography>BR91905</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<div className='overflow-x-auto border rounded'>
|
||||
<table className={tableStyles.table}>
|
||||
<thead className='border-bs-0'>
|
||||
<tr>
|
||||
<th className='!bg-transparent'>Item</th>
|
||||
<th className='!bg-transparent'>Description</th>
|
||||
<th className='!bg-transparent'>Hours</th>
|
||||
<th className='!bg-transparent'>Qty</th>
|
||||
<th className='!bg-transparent'>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<Typography color='text.primary'>{item.Item}</Typography>
|
||||
</td>
|
||||
<td>
|
||||
<Typography color='text.primary'>{item.Description}</Typography>
|
||||
</td>
|
||||
<td>
|
||||
<Typography color='text.primary'>{item.Hours}</Typography>
|
||||
</td>
|
||||
<td>
|
||||
<Typography color='text.primary'>{item.Qty}</Typography>
|
||||
</td>
|
||||
<td>
|
||||
<Typography color='text.primary'>{item.Total}</Typography>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<div className='flex justify-between flex-col gap-y-4 sm:flex-row'>
|
||||
<div className='flex flex-col gap-1 order-2 sm:order-[unset]'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
Salesperson:
|
||||
</Typography>
|
||||
<Typography>Tommy Shelby</Typography>
|
||||
</div>
|
||||
<Typography>Thanks for your business</Typography>
|
||||
</div>
|
||||
<div className='min-is-[200px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography>Subtotal:</Typography>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
$1800
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography>Discount:</Typography>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
$28
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography>Tax:</Typography>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
21%
|
||||
</Typography>
|
||||
</div>
|
||||
<Divider className='mlb-2' />
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography>Total:</Typography>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
$1690
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Divider className='border-dashed' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Typography>
|
||||
<Typography component='span' className='font-medium' color='text.primary'>
|
||||
Note:
|
||||
</Typography>{' '}
|
||||
It was a pleasure working with you and your team. We hope you will keep us in mind for future freelance
|
||||
projects. Thank You!
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default PreviewCard
|
||||
@@ -1,31 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Type Imports
|
||||
import type { InvoiceType } from '@/types/apps/invoiceTypes'
|
||||
|
||||
// Component Imports
|
||||
import PreviewActions from './PreviewActions'
|
||||
import PreviewCard from './PreviewCard'
|
||||
|
||||
const Preview = ({ invoiceData, id }: { invoiceData?: InvoiceType; id: string }) => {
|
||||
// Handle Print Button Click
|
||||
const handleButtonClick = () => {
|
||||
window.print()
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, md: 9 }}>
|
||||
<PreviewCard invoiceData={invoiceData} id={id} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<PreviewActions id={id} onButtonClick={handleButtonClick} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default Preview
|
||||
@@ -1,31 +0,0 @@
|
||||
@media print {
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
#__next :is(.sm\:\!p-12) {
|
||||
padding: 0rem !important;
|
||||
}
|
||||
[data-dark] {
|
||||
--mui-palette-text-primary: rgba(47, 43, 61, 0.9);
|
||||
--mui-palette-action-hover: rgba(47, 43, 61, 0.06);
|
||||
--mui-palette-text-secondary: rgba(47, 43, 61, 0.7);
|
||||
--mui-palette-divider: rgba(47, 43, 61, 0.12);
|
||||
}
|
||||
|
||||
/* Only show the .preview-card element when printing */
|
||||
.previewCard * {
|
||||
visibility: visible;
|
||||
}
|
||||
.previewCard {
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.previewCard {
|
||||
position: relative !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { FormEvent } from 'react'
|
||||
|
||||
// MUI Import
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import InputAdornment from '@mui/material/InputAdornment'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Button from '@mui/material/Button'
|
||||
import Divider from '@mui/material/Divider'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Styled Component Imports
|
||||
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
handleClose: () => void
|
||||
}
|
||||
|
||||
type FormDataType = {
|
||||
paymentDate: Date
|
||||
paymentMethod: string
|
||||
paymentAmount: number
|
||||
paymentNote: string
|
||||
}
|
||||
|
||||
// Vars
|
||||
const initialData: FormDataType = {
|
||||
paymentDate: new Date(),
|
||||
paymentMethod: 'select-method',
|
||||
paymentAmount: 500,
|
||||
paymentNote: ''
|
||||
}
|
||||
|
||||
const AddPaymentDrawer = ({ open, handleClose }: Props) => {
|
||||
// States
|
||||
const [formData, setFormData] = useState<FormDataType>(initialData)
|
||||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
handleClose()
|
||||
setFormData(initialData)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
handleClose()
|
||||
setFormData(initialData)
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
anchor='right'
|
||||
variant='temporary'
|
||||
onClose={handleReset}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
|
||||
>
|
||||
<div className='flex items-center justify-between plb-5 pli-6'>
|
||||
<Typography variant='h5'>Add New User</Typography>
|
||||
<IconButton size='small' onClick={handleReset}>
|
||||
<i className='tabler-x text-2xl text-textPrimary' />
|
||||
</IconButton>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className='p-6'>
|
||||
<form onSubmit={handleSubmit} className='flex flex-col gap-6'>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
id='invoice-balance'
|
||||
label='Invoice Balance'
|
||||
slotProps={{
|
||||
input: {
|
||||
disabled: true
|
||||
}
|
||||
}}
|
||||
defaultValue='5000.00'
|
||||
/>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
id='payment-amount'
|
||||
label='Payment Amount'
|
||||
type='number'
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: <InputAdornment position='start'>$</InputAdornment>
|
||||
}
|
||||
}}
|
||||
value={formData.paymentAmount}
|
||||
onChange={e => setFormData({ ...formData, paymentAmount: +e.target.value })}
|
||||
/>
|
||||
<AppReactDatepicker
|
||||
selected={formData.paymentDate}
|
||||
id='payment-date'
|
||||
onChange={(date: Date | null) => date !== null && setFormData({ ...formData, paymentDate: date })}
|
||||
customInput={<CustomTextField fullWidth label='Payment Date' />}
|
||||
/>
|
||||
<CustomTextField
|
||||
select
|
||||
label='Payment Method'
|
||||
id='payment-method-select'
|
||||
value={formData.paymentMethod}
|
||||
onChange={e => setFormData({ ...formData, paymentMethod: e.target.value as string })}
|
||||
>
|
||||
<MenuItem value='select-method' disabled>
|
||||
Select Payment Method
|
||||
</MenuItem>
|
||||
<MenuItem value='cash'>Cash</MenuItem>
|
||||
<MenuItem value='bank-transfer'>Bank Transfer</MenuItem>
|
||||
<MenuItem value='credit'>Credit</MenuItem>
|
||||
<MenuItem value='debit'>Debit</MenuItem>
|
||||
<MenuItem value='paypal'>Paypal</MenuItem>
|
||||
</CustomTextField>
|
||||
<CustomTextField
|
||||
rows={6}
|
||||
multiline
|
||||
fullWidth
|
||||
label='Internal Payment Note'
|
||||
placeholder='Internal Payment Note'
|
||||
value={formData.paymentNote}
|
||||
onChange={e => setFormData({ ...formData, paymentNote: e.target.value })}
|
||||
/>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Button variant='contained' type='submit'>
|
||||
Send
|
||||
</Button>
|
||||
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddPaymentDrawer
|
||||
@@ -1,127 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { FormEvent } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Button from '@mui/material/Button'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Divider from '@mui/material/Divider'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
handleClose: () => void
|
||||
}
|
||||
|
||||
type FormDataType = {
|
||||
from: string
|
||||
to: string
|
||||
subject: string
|
||||
message: string
|
||||
}
|
||||
|
||||
// Vars
|
||||
const initialData: FormDataType = {
|
||||
from: 'shelbyComapny@email.com',
|
||||
to: 'qConsolidated@email.com',
|
||||
subject: 'Invoice of purchased Admin Templates',
|
||||
message: `Dear Queen Consolidated,
|
||||
|
||||
Thank you for your business, always a pleasure to work with you!
|
||||
|
||||
We have generated a new invoice in the amount of $95.59
|
||||
|
||||
We would appreciate payment of this invoice by 05/11/2019`
|
||||
}
|
||||
|
||||
const SendInvoiceDrawer = ({ open, handleClose }: Props) => {
|
||||
// States
|
||||
const [formData, setFormData] = useState<FormDataType>(initialData)
|
||||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
handleClose()
|
||||
setFormData(initialData)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
handleClose()
|
||||
setFormData(initialData)
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
anchor='right'
|
||||
variant='temporary'
|
||||
onClose={handleReset}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
|
||||
>
|
||||
<div className='flex items-center justify-between plb-5 pli-6'>
|
||||
<Typography variant='h5'>Send Invoice</Typography>
|
||||
<IconButton size='small' onClick={handleReset}>
|
||||
<i className='tabler-x text-2xl text-textPrimary' />
|
||||
</IconButton>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className='p-6'>
|
||||
<form onSubmit={handleSubmit} className='flex flex-col items-start gap-6'>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='From'
|
||||
variant='outlined'
|
||||
value={formData.from}
|
||||
onChange={e => setFormData({ ...formData, from: e.target.value })}
|
||||
/>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='To'
|
||||
variant='outlined'
|
||||
value={formData.to}
|
||||
onChange={e => setFormData({ ...formData, to: e.target.value })}
|
||||
/>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Subject'
|
||||
variant='outlined'
|
||||
value={formData.subject}
|
||||
onChange={e => setFormData({ ...formData, subject: e.target.value })}
|
||||
/>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Message'
|
||||
variant='outlined'
|
||||
multiline
|
||||
rows={10}
|
||||
value={formData.message}
|
||||
onChange={e => setFormData({ ...formData, message: e.target.value })}
|
||||
/>
|
||||
<Chip
|
||||
size='small'
|
||||
color='primary'
|
||||
variant='tonal'
|
||||
className='rounded'
|
||||
label='Invoice Attached'
|
||||
icon={<i className='tabler-link' />}
|
||||
/>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Button variant='contained' color='primary' type='submit'>
|
||||
Send
|
||||
</Button>
|
||||
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default SendInvoiceDrawer
|
||||
@@ -1,85 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import type { RefObject } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// Third-party imports
|
||||
import { useDragAndDrop } from '@formkit/drag-and-drop/react'
|
||||
import { animations } from '@formkit/drag-and-drop'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
// Type Imports
|
||||
import type { RootState } from '@/redux-store'
|
||||
|
||||
// Slice Imports
|
||||
import { addColumn, updateColumns } from '@/redux-store/slices/kanban'
|
||||
|
||||
// Component Imports
|
||||
import KanbanList from './KanbanList'
|
||||
import NewColumn from './NewColumn'
|
||||
import KanbanDrawer from './KanbanDrawer'
|
||||
|
||||
const KanbanBoard = () => {
|
||||
// State
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const kanbanStore = useSelector((state: RootState) => state.kanbanReducer)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const [boardRef, columns, setColumns] = useDragAndDrop(kanbanStore.columns, {
|
||||
plugins: [animations()],
|
||||
dragHandle: '.list-handle'
|
||||
})
|
||||
|
||||
// Add New Column
|
||||
const addNewColumn = (title: string) => {
|
||||
const maxId = Math.max(...kanbanStore.columns.map(column => column.id))
|
||||
|
||||
dispatch(addColumn(title))
|
||||
setColumns([...columns, { id: maxId + 1, title, taskIds: [] }])
|
||||
}
|
||||
|
||||
// To get the current task for the drawer
|
||||
const currentTask = kanbanStore.tasks.find(task => task.id === kanbanStore.currentTaskId)
|
||||
|
||||
// Update Columns on Drag and Drop
|
||||
useEffect(() => {
|
||||
if (columns !== kanbanStore.columns) dispatch(updateColumns(columns))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [columns])
|
||||
|
||||
return (
|
||||
<div className='flex items-start gap-6'>
|
||||
<div ref={boardRef as RefObject<HTMLDivElement>} className='flex gap-6'>
|
||||
{columns.map(column => (
|
||||
<KanbanList
|
||||
key={column.id}
|
||||
dispatch={dispatch}
|
||||
column={column}
|
||||
store={kanbanStore}
|
||||
setDrawerOpen={setDrawerOpen}
|
||||
columns={columns}
|
||||
setColumns={setColumns}
|
||||
currentTask={currentTask}
|
||||
tasks={column.taskIds.map(taskId => kanbanStore.tasks.find(task => task.id === taskId))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<NewColumn addNewColumn={addNewColumn} />
|
||||
{currentTask && (
|
||||
<KanbanDrawer
|
||||
task={currentTask}
|
||||
drawerOpen={drawerOpen}
|
||||
setDrawerOpen={setDrawerOpen}
|
||||
dispatch={dispatch}
|
||||
columns={columns}
|
||||
setColumns={setColumns}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KanbanBoard
|
||||
@@ -1,268 +0,0 @@
|
||||
// React Imports
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import type { ChangeEvent } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Button from '@mui/material/Button'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import InputAdornment from '@mui/material/InputAdornment'
|
||||
|
||||
// Third-party Imports
|
||||
import { useForm, Controller } from 'react-hook-form'
|
||||
import { valibotResolver } from '@hookform/resolvers/valibot'
|
||||
import { minLength, nonEmpty, object, pipe, string } from 'valibot'
|
||||
import type { InferInput } from 'valibot'
|
||||
|
||||
// Type Imports
|
||||
import type { ColumnType, TaskType } from '@/types/apps/kanbanTypes'
|
||||
import type { AppDispatch } from '@/redux-store'
|
||||
|
||||
// Slice Imports
|
||||
import { editTask, deleteTask } from '@/redux-store/slices/kanban'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
|
||||
|
||||
// Data Imports
|
||||
import { chipColor } from './TaskCard'
|
||||
|
||||
type KanbanDrawerProps = {
|
||||
drawerOpen: boolean
|
||||
dispatch: AppDispatch
|
||||
setDrawerOpen: (value: boolean) => void
|
||||
task: TaskType
|
||||
columns: ColumnType[]
|
||||
setColumns: (value: ColumnType[]) => void
|
||||
}
|
||||
|
||||
type FormData = InferInput<typeof schema>
|
||||
|
||||
const schema = object({
|
||||
title: pipe(string(), nonEmpty('Title is required'), minLength(1))
|
||||
})
|
||||
|
||||
const KanbanDrawer = (props: KanbanDrawerProps) => {
|
||||
// Props
|
||||
const { drawerOpen, dispatch, setDrawerOpen, task, columns, setColumns } = props
|
||||
|
||||
// States
|
||||
const [date, setDate] = useState<Date | undefined>(task.dueDate)
|
||||
const [badgeText, setBadgeText] = useState(task.badgeText || [])
|
||||
const [fileName, setFileName] = useState<string>('')
|
||||
const [comment, setComment] = useState<string>('')
|
||||
|
||||
// Refs
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Hooks
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors }
|
||||
} = useForm<FormData>({
|
||||
defaultValues: {
|
||||
title: task.title
|
||||
},
|
||||
resolver: valibotResolver(schema)
|
||||
})
|
||||
|
||||
// Handle File Upload
|
||||
const handleFileUpload = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { files } = event.target
|
||||
|
||||
if (files && files.length !== 0) {
|
||||
setFileName(files[0].name)
|
||||
}
|
||||
}
|
||||
|
||||
// Close Drawer
|
||||
const handleClose = () => {
|
||||
setDrawerOpen(false)
|
||||
reset({ title: task.title })
|
||||
setBadgeText(task.badgeText || [])
|
||||
setDate(task.dueDate)
|
||||
setFileName('')
|
||||
setComment('')
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// Update Task
|
||||
const updateTask = (data: FormData) => {
|
||||
dispatch(editTask({ id: task.id, title: data.title, badgeText, dueDate: date }))
|
||||
handleClose()
|
||||
}
|
||||
|
||||
// Handle Reset
|
||||
const handleReset = () => {
|
||||
setDrawerOpen(false)
|
||||
dispatch(deleteTask(task.id))
|
||||
|
||||
const updatedColumns = columns.map(column => {
|
||||
return {
|
||||
...column,
|
||||
taskIds: column.taskIds.filter(taskId => taskId !== task.id)
|
||||
}
|
||||
})
|
||||
|
||||
setColumns(updatedColumns)
|
||||
}
|
||||
|
||||
// To set the initial values according to the task
|
||||
useEffect(() => {
|
||||
reset({ title: task.title })
|
||||
setBadgeText(task.badgeText || [])
|
||||
setDate(task.dueDate)
|
||||
}, [task, reset])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
anchor='right'
|
||||
variant='temporary'
|
||||
ModalProps={{ keepMounted: true }}
|
||||
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className='flex justify-between items-center pli-6 plb-5 border-be'>
|
||||
<Typography variant='h5'>Edit Task</Typography>
|
||||
<IconButton size='small' onClick={handleClose}>
|
||||
<i className='tabler-x text-2xl text-textPrimary' />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className='p-6'>
|
||||
<form className='flex flex-col gap-y-5' onSubmit={handleSubmit(updateTask)}>
|
||||
<Controller
|
||||
name='title'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Title'
|
||||
{...field}
|
||||
error={Boolean(errors.title)}
|
||||
helperText={errors.title?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<AppReactDatepicker
|
||||
selected={date ? new Date(date) : new Date()}
|
||||
id='basic-input'
|
||||
onChange={(date: Date | null) => {
|
||||
date !== null && setDate(date)
|
||||
}}
|
||||
placeholderText='Click to select a date'
|
||||
dateFormat={'d MMMM, yyyy'}
|
||||
customInput={<CustomTextField label='Due Date' fullWidth />}
|
||||
/>
|
||||
<CustomTextField
|
||||
select
|
||||
label='Label'
|
||||
slotProps={{
|
||||
select: {
|
||||
multiple: true,
|
||||
value: badgeText || [],
|
||||
onChange: e => setBadgeText(e.target.value as string[]),
|
||||
renderValue: selected => (
|
||||
<div className='flex flex-wrap gap-1'>
|
||||
{(selected as string[]).map(value => (
|
||||
<Chip
|
||||
variant='tonal'
|
||||
key={value}
|
||||
size='small'
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
label={value}
|
||||
color={chipColor[value]?.color}
|
||||
onDelete={() => setBadgeText(current => current.filter(item => item !== value))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Object.keys(chipColor).map(chip => (
|
||||
<MenuItem key={chip} value={chip}>
|
||||
<Checkbox checked={badgeText && badgeText.indexOf(chip) > -1} />
|
||||
<ListItemText primary={chip} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</CustomTextField>
|
||||
<div>
|
||||
<Typography variant='caption' color='text.primary'>
|
||||
Assigned
|
||||
</Typography>
|
||||
<div className='flex gap-1'>
|
||||
{task.assigned?.map((avatar, index) => (
|
||||
<Tooltip title={avatar.name} key={index}>
|
||||
<CustomAvatar key={index} src={avatar.src} size={26} className='cursor-pointer' />
|
||||
</Tooltip>
|
||||
))}
|
||||
<CustomAvatar size={26} className='cursor-pointer'>
|
||||
<i className='tabler-plus text-base text-textSecondary' />
|
||||
</CustomAvatar>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
placeholder='Choose File'
|
||||
variant='outlined'
|
||||
value={fileName}
|
||||
slotProps={{
|
||||
input: {
|
||||
readOnly: true,
|
||||
endAdornment: fileName ? (
|
||||
<InputAdornment position='end'>
|
||||
<IconButton size='small' edge='end' onClick={() => setFileName('')}>
|
||||
<i className='tabler-x' />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : null
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button component='label' variant='tonal' htmlFor='contained-button-file'>
|
||||
Choose
|
||||
<input hidden id='contained-button-file' type='file' onChange={handleFileUpload} ref={fileInputRef} />
|
||||
</Button>
|
||||
</div>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Comment'
|
||||
value={comment}
|
||||
onChange={e => setComment(e.target.value)}
|
||||
multiline
|
||||
rows={4}
|
||||
placeholder='Write a Comment....'
|
||||
/>
|
||||
<div className='flex gap-4'>
|
||||
<Button variant='contained' color='primary' type='submit'>
|
||||
Update
|
||||
</Button>
|
||||
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KanbanDrawer
|
||||
@@ -1,215 +0,0 @@
|
||||
// React Imports
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { FormEvent, RefObject } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Typography from '@mui/material/Typography'
|
||||
import InputBase from '@mui/material/InputBase'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
|
||||
// Third-party imports
|
||||
import { useDragAndDrop } from '@formkit/drag-and-drop/react'
|
||||
import { animations } from '@formkit/drag-and-drop'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { TaskType, ColumnType, KanbanType } from '@/types/apps/kanbanTypes'
|
||||
import type { AppDispatch } from '@/redux-store'
|
||||
|
||||
// Slice Imports
|
||||
import { addTask, editColumn, deleteColumn, updateColumnTaskIds } from '@/redux-store/slices/kanban'
|
||||
|
||||
// Component Imports
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
import TaskCard from './TaskCard'
|
||||
import NewTask from './NewTask'
|
||||
|
||||
// Styles Imports
|
||||
import styles from './styles.module.css'
|
||||
|
||||
type KanbanListProps = {
|
||||
column: ColumnType
|
||||
tasks: (TaskType | undefined)[]
|
||||
dispatch: AppDispatch
|
||||
store: KanbanType
|
||||
setDrawerOpen: (value: boolean) => void
|
||||
columns: ColumnType[]
|
||||
setColumns: (value: ColumnType[]) => void
|
||||
currentTask: TaskType | undefined
|
||||
}
|
||||
|
||||
const KanbanList = (props: KanbanListProps) => {
|
||||
// Props
|
||||
const { column, tasks, dispatch, store, setDrawerOpen, columns, setColumns, currentTask } = props
|
||||
|
||||
// States
|
||||
const [editDisplay, setEditDisplay] = useState(false)
|
||||
const [title, setTitle] = useState(column.title)
|
||||
|
||||
// Hooks
|
||||
const [tasksListRef, tasksList, setTasksList] = useDragAndDrop(tasks, {
|
||||
group: 'tasksList',
|
||||
plugins: [animations()],
|
||||
draggable: el => el.classList.contains('item-draggable')
|
||||
})
|
||||
|
||||
// Add New Task
|
||||
const addNewTask = (title: string) => {
|
||||
dispatch(addTask({ columnId: column.id, title: title }))
|
||||
|
||||
setTasksList([...tasksList, { id: store.tasks[store.tasks.length - 1].id + 1, title }])
|
||||
|
||||
const newColumns = columns.map(col => {
|
||||
if (col.id === column.id) {
|
||||
return { ...col, taskIds: [...col.taskIds, store.tasks[store.tasks.length - 1].id + 1] }
|
||||
}
|
||||
|
||||
return col
|
||||
})
|
||||
|
||||
setColumns(newColumns)
|
||||
}
|
||||
|
||||
// Handle Submit Edit
|
||||
const handleSubmitEdit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setEditDisplay(!editDisplay)
|
||||
dispatch(editColumn({ id: column.id, title }))
|
||||
|
||||
const newColumn = columns.map(col => {
|
||||
if (col.id === column.id) {
|
||||
return { ...col, title }
|
||||
}
|
||||
|
||||
return col
|
||||
})
|
||||
|
||||
setColumns(newColumn)
|
||||
}
|
||||
|
||||
// Cancel Edit
|
||||
const cancelEdit = () => {
|
||||
setEditDisplay(!editDisplay)
|
||||
setTitle(column.title)
|
||||
}
|
||||
|
||||
// Delete Column
|
||||
const handleDeleteColumn = () => {
|
||||
dispatch(deleteColumn({ columnId: column.id }))
|
||||
setColumns(columns.filter(col => col.id !== column.id))
|
||||
}
|
||||
|
||||
// Update column taskIds on drag and drop
|
||||
useEffect(() => {
|
||||
if (tasksList !== tasks) {
|
||||
dispatch(updateColumnTaskIds({ id: column.id, tasksList }))
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tasksList])
|
||||
|
||||
// To update the tasksList when a task is edited
|
||||
useEffect(() => {
|
||||
const newTasks = tasksList.map(task => {
|
||||
if (task?.id === currentTask?.id) {
|
||||
return currentTask
|
||||
}
|
||||
|
||||
return task
|
||||
})
|
||||
|
||||
if (currentTask !== tasksList.find(task => task?.id === currentTask?.id)) {
|
||||
setTasksList(newTasks)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentTask])
|
||||
|
||||
// To update the tasksList when columns are updated
|
||||
useEffect(() => {
|
||||
let taskIds: ColumnType['taskIds'] = []
|
||||
|
||||
columns.map(col => {
|
||||
taskIds = [...taskIds, ...col.taskIds]
|
||||
})
|
||||
|
||||
const newTasksList = tasksList.filter(task => task && taskIds.includes(task.id))
|
||||
|
||||
setTasksList(newTasksList)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [columns])
|
||||
|
||||
return (
|
||||
<div ref={tasksListRef as RefObject<HTMLDivElement>} className='flex flex-col is-[16.5rem]'>
|
||||
{editDisplay ? (
|
||||
<form
|
||||
className='flex items-center mbe-4'
|
||||
onSubmit={handleSubmitEdit}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Escape') {
|
||||
cancelEdit()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<InputBase value={title} autoFocus onChange={e => setTitle(e.target.value)} required className='flex-auto' />
|
||||
<IconButton color='success' size='small' type='submit'>
|
||||
<i className='tabler-check' />
|
||||
</IconButton>
|
||||
<IconButton color='error' size='small' type='reset' onClick={cancelEdit}>
|
||||
<i className='tabler-x' />
|
||||
</IconButton>
|
||||
</form>
|
||||
) : (
|
||||
<div
|
||||
id='no-drag'
|
||||
className={classnames(
|
||||
'flex items-center justify-between is-[16.5rem] bs-[2.125rem] mbe-4',
|
||||
styles.kanbanColumn
|
||||
)}
|
||||
>
|
||||
<Typography variant='h5' noWrap className='max-is-[80%]'>
|
||||
{column.title}
|
||||
</Typography>
|
||||
<div className='flex items-center'>
|
||||
<i className={classnames('tabler-arrows-move text-textSecondary list-handle', styles.drag)} />
|
||||
<OptionMenu
|
||||
iconClassName='text-xl text-textPrimary'
|
||||
options={[
|
||||
{
|
||||
text: 'Edit',
|
||||
icon: 'tabler-pencil',
|
||||
menuItemProps: {
|
||||
className: 'flex items-center gap-2',
|
||||
onClick: () => setEditDisplay(!editDisplay)
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Delete',
|
||||
icon: 'tabler-trash',
|
||||
menuItemProps: { className: 'flex items-center gap-2', onClick: handleDeleteColumn }
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{tasksList.map(
|
||||
task =>
|
||||
task && (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
dispatch={dispatch}
|
||||
column={column}
|
||||
setColumns={setColumns}
|
||||
columns={columns}
|
||||
setDrawerOpen={setDrawerOpen}
|
||||
tasksList={tasksList}
|
||||
setTasksList={setTasksList}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<NewTask addTask={addNewTask} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KanbanList
|
||||
@@ -1,118 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Button from '@mui/material/Button'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Third-party Imports
|
||||
import { useForm, Controller } from 'react-hook-form'
|
||||
import { valibotResolver } from '@hookform/resolvers/valibot'
|
||||
import { object, string, minLength, pipe, nonEmpty } from 'valibot'
|
||||
import type { InferInput } from 'valibot'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Styled CustomTextField component
|
||||
const CustomTextFieldStyled = styled(CustomTextField)({
|
||||
'& .MuiInputBase-root.MuiFilledInput-root': {
|
||||
backgroundColor: 'var(--mui-palette-background-paper) !important'
|
||||
}
|
||||
})
|
||||
|
||||
type FormData = InferInput<typeof schema>
|
||||
|
||||
const schema = object({
|
||||
title: pipe(string(), nonEmpty('Title is required'), minLength(1))
|
||||
})
|
||||
|
||||
const NewColumn = ({ addNewColumn }: { addNewColumn: (title: string) => void }) => {
|
||||
// States
|
||||
const [display, setDisplay] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors }
|
||||
} = useForm<FormData>({
|
||||
defaultValues: {
|
||||
title: ''
|
||||
},
|
||||
resolver: valibotResolver(schema)
|
||||
})
|
||||
|
||||
// Display the Add New form
|
||||
const toggleDisplay = () => {
|
||||
setDisplay(!display)
|
||||
}
|
||||
|
||||
// Handle the Add New form
|
||||
const onSubmit = (data: FormData) => {
|
||||
addNewColumn(data.title)
|
||||
setDisplay(false)
|
||||
reset({ title: '' })
|
||||
}
|
||||
|
||||
// Handle reset
|
||||
const handleReset = () => {
|
||||
toggleDisplay()
|
||||
reset({ title: '' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4 items-start min-is-[16.5rem] is-[16.5rem]'>
|
||||
<Typography variant='h5' onClick={toggleDisplay} className='flex items-center gap-1 cursor-pointer'>
|
||||
<i className='tabler-plus text-base' />
|
||||
<span className='whitespace-nowrap'>Add New</span>
|
||||
</Typography>
|
||||
{display && (
|
||||
<form
|
||||
className='flex flex-col gap-4 is-[16.5rem]'
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Escape') {
|
||||
handleReset()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name='title'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<CustomTextFieldStyled
|
||||
fullWidth
|
||||
autoFocus
|
||||
variant='outlined'
|
||||
placeholder='Board Title'
|
||||
{...field}
|
||||
error={Boolean(errors.title)}
|
||||
helperText={errors.title ? errors.title.message : null}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className='flex gap-3'>
|
||||
<Button variant='contained' size='small' color='primary' type='submit'>
|
||||
Add
|
||||
</Button>
|
||||
<Button
|
||||
variant='tonal'
|
||||
size='small'
|
||||
color='secondary'
|
||||
onClick={() => {
|
||||
handleReset()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewColumn
|
||||
@@ -1,122 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Button from '@mui/material/Button'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Third-party Imports
|
||||
import { useForm, Controller } from 'react-hook-form'
|
||||
import { valibotResolver } from '@hookform/resolvers/valibot'
|
||||
import { object, string, minLength, pipe, nonEmpty } from 'valibot'
|
||||
import type { InferInput } from 'valibot'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Styled CustomTextField component
|
||||
const CustomTextFieldStyled = styled(CustomTextField)({
|
||||
'& .MuiInputBase-root.MuiFilledInput-root': {
|
||||
backgroundColor: 'var(--mui-palette-background-paper) !important'
|
||||
}
|
||||
})
|
||||
|
||||
type FormData = InferInput<typeof schema>
|
||||
|
||||
const schema = object({
|
||||
content: pipe(string(), nonEmpty('Content is required'), minLength(1))
|
||||
})
|
||||
|
||||
const NewTask = ({ addTask }: { addTask: (content: string) => void }) => {
|
||||
// States
|
||||
const [displayNewItem, setDisplayNewItem] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors }
|
||||
} = useForm<FormData>({
|
||||
defaultValues: {
|
||||
content: ''
|
||||
},
|
||||
resolver: valibotResolver(schema)
|
||||
})
|
||||
|
||||
// Display the Add New Task form
|
||||
const toggleDisplay = () => {
|
||||
setDisplayNewItem(!displayNewItem)
|
||||
}
|
||||
|
||||
// Handle the Add New Task form
|
||||
const onSubmit = (data: FormData) => {
|
||||
addTask(data.content)
|
||||
setDisplayNewItem(false)
|
||||
reset({ content: '' })
|
||||
}
|
||||
|
||||
// Handle reset
|
||||
const handleReset = () => {
|
||||
toggleDisplay()
|
||||
reset({ content: '' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4 items-start'>
|
||||
<Typography onClick={toggleDisplay} color='text.primary' className='flex items-center gap-1 cursor-pointer'>
|
||||
<i className='tabler-plus text-base' />
|
||||
<span>Add New Item</span>
|
||||
</Typography>
|
||||
{displayNewItem && (
|
||||
<form className='flex flex-col gap-4 min-is-[16.5rem]' onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
name='content'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<CustomTextFieldStyled
|
||||
fullWidth
|
||||
multiline
|
||||
autoFocus
|
||||
rows={2}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmit(onSubmit)(e)
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
handleReset()
|
||||
}
|
||||
}}
|
||||
placeholder='Add Content'
|
||||
variant='outlined'
|
||||
{...field}
|
||||
error={Boolean(errors.content)}
|
||||
helperText={errors.content ? errors.content.message : null}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className='flex gap-3'>
|
||||
<Button variant='contained' size='small' color='primary' type='submit'>
|
||||
Add
|
||||
</Button>
|
||||
<Button
|
||||
variant='tonal'
|
||||
size='small'
|
||||
color='secondary'
|
||||
onClick={() => {
|
||||
handleReset()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewTask
|
||||
@@ -1,202 +0,0 @@
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import AvatarGroup from '@mui/material/AvatarGroup'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Menu from '@mui/material/Menu'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
|
||||
// Third-Party Imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { ColumnType, TaskType } from '@/types/apps/kanbanTypes'
|
||||
import type { AppDispatch } from '@/redux-store'
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Slice Imports
|
||||
import { getCurrentTask, deleteTask } from '@/redux-store/slices/kanban'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
|
||||
// Styles Imports
|
||||
import styles from './styles.module.css'
|
||||
|
||||
type chipColorType = {
|
||||
color: ThemeColor
|
||||
}
|
||||
|
||||
type TaskCardProps = {
|
||||
task: TaskType
|
||||
dispatch: AppDispatch
|
||||
column: ColumnType
|
||||
setColumns: (value: ColumnType[]) => void
|
||||
columns: ColumnType[]
|
||||
setDrawerOpen: (value: boolean) => void
|
||||
tasksList: (TaskType | undefined)[]
|
||||
setTasksList: (value: (TaskType | undefined)[]) => void
|
||||
}
|
||||
|
||||
export const chipColor: { [key: string]: chipColorType } = {
|
||||
UX: { color: 'success' },
|
||||
'Code Review': { color: 'error' },
|
||||
Dashboard: { color: 'info' },
|
||||
Images: { color: 'warning' },
|
||||
App: { color: 'secondary' },
|
||||
'Charts & Map': { color: 'primary' }
|
||||
}
|
||||
|
||||
const TaskCard = (props: TaskCardProps) => {
|
||||
// Props
|
||||
const { task, dispatch, column, setColumns, columns, setDrawerOpen, tasksList, setTasksList } = props
|
||||
|
||||
// States
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
// Handle menu click
|
||||
const handleClick = (e: any) => {
|
||||
setMenuOpen(true)
|
||||
setAnchorEl(e.currentTarget)
|
||||
}
|
||||
|
||||
// Handle menu close
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
setMenuOpen(false)
|
||||
}
|
||||
|
||||
// Handle Task Click
|
||||
const handleTaskClick = () => {
|
||||
setDrawerOpen(true)
|
||||
dispatch(getCurrentTask(task.id))
|
||||
}
|
||||
|
||||
// Delete Task
|
||||
const handleDeleteTask = () => {
|
||||
dispatch(deleteTask(task.id))
|
||||
setTasksList(tasksList.filter(taskItem => taskItem?.id !== task.id))
|
||||
|
||||
const newTaskIds = column.taskIds.filter(taskId => taskId !== task.id)
|
||||
const newColumn = { ...column, taskIds: newTaskIds }
|
||||
const newColumns = columns.map(col => (col.id === column.id ? newColumn : col))
|
||||
|
||||
setColumns(newColumns)
|
||||
}
|
||||
|
||||
// Handle Delete
|
||||
const handleDelete = () => {
|
||||
handleClose()
|
||||
handleDeleteTask()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
className={classnames(
|
||||
'item-draggable is-[16.5rem] cursor-grab active:cursor-grabbing overflow-visible mbe-4',
|
||||
styles.card
|
||||
)}
|
||||
onClick={() => handleTaskClick()}
|
||||
>
|
||||
<CardContent className='flex flex-col gap-y-2 items-start relative overflow-hidden'>
|
||||
{task.badgeText && task.badgeText.length > 0 && (
|
||||
<div className='flex flex-wrap items-center justify-start gap-2 is-full max-is-[85%]'>
|
||||
{task.badgeText.map(
|
||||
(badge, index) =>
|
||||
chipColor[badge]?.color && (
|
||||
<Chip variant='tonal' key={index} label={badge} size='small' color={chipColor[badge].color} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className='absolute block-start-4 inline-end-3' onClick={e => e.stopPropagation()}>
|
||||
<IconButton
|
||||
aria-label='more'
|
||||
size='small'
|
||||
className={classnames(styles.menu, {
|
||||
[styles.menuOpen]: menuOpen
|
||||
})}
|
||||
aria-controls='long-menu'
|
||||
aria-haspopup='true'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<i className='tabler-dots-vertical' />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id='long-menu'
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<MenuItem onClick={handleClose}>Duplicate Task</MenuItem>
|
||||
<MenuItem onClick={handleClose}>Copy Task Link</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleDelete()
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
{task.image && <img src={task.image} alt='task Image' className='is-full rounded' />}
|
||||
<Typography color='text.primary' className='max-is-[85%] break-words'>
|
||||
{task.title}
|
||||
</Typography>
|
||||
{(task.attachments !== undefined && task.attachments > 0) ||
|
||||
(task.comments !== undefined && task.comments > 0) ||
|
||||
(task.assigned !== undefined && task.assigned.length > 0) ? (
|
||||
<div className='flex justify-between items-center gap-4 is-full'>
|
||||
{(task.attachments !== undefined && task.attachments > 0) ||
|
||||
(task.comments !== undefined && task.comments > 0) ? (
|
||||
<div className='flex gap-4'>
|
||||
{task.attachments !== undefined && task.attachments > 0 && (
|
||||
<div className='flex items-center gap-1'>
|
||||
<i className='tabler-paperclip text-xl text-textSecondary' />
|
||||
<Typography color='text.secondary'>{task.attachments}</Typography>
|
||||
</div>
|
||||
)}
|
||||
{task.comments !== undefined && task.comments > 0 && (
|
||||
<div className='flex items-center gap-1'>
|
||||
<i className='tabler-message-2 text-xl text-textSecondary' />
|
||||
<Typography color='text.secondary'>{task.comments}</Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{task.assigned !== undefined && task.assigned.length > 0 && (
|
||||
<AvatarGroup max={4} className='pull-up'>
|
||||
{task.assigned?.map((avatar, index) => (
|
||||
<Tooltip title={avatar.name} key={index}>
|
||||
<CustomAvatar
|
||||
key={index}
|
||||
src={avatar.src}
|
||||
alt={avatar.name}
|
||||
size={26}
|
||||
className='cursor-pointer'
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</AvatarGroup>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TaskCard
|
||||
@@ -1,42 +0,0 @@
|
||||
.kanbanColumn {
|
||||
&:hover .drag {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.drag {
|
||||
display: none;
|
||||
cursor: grab;
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
&:hover {
|
||||
.menu {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
}
|
||||
.menu {
|
||||
display: none !important;
|
||||
|
||||
&.menuOpen {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scroll::-webkit-scrollbar {
|
||||
inline-size: 6px;
|
||||
block-size: 6px;
|
||||
}
|
||||
|
||||
.scroll::-webkit-scrollbar-track {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.scroll::-webkit-scrollbar-thumb {
|
||||
background-color: var(--mui-palette-divider);
|
||||
border-radius: 6px;
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// Next Imports
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Third Party Imports
|
||||
import type { ApexOptions } from 'apexcharts'
|
||||
|
||||
// Components Imports
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
// Styled Component Imports
|
||||
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
|
||||
|
||||
const deliveryExceptionsChartSeries = [13, 25, 22, 40]
|
||||
|
||||
const LogisticsDeliveryExceptions = () => {
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
|
||||
const options: ApexOptions = {
|
||||
labels: ['Incorrect address', 'Weather conditions', 'Federal Holidays', 'Damage during transit'],
|
||||
stroke: {
|
||||
width: 0
|
||||
},
|
||||
colors: [
|
||||
'var(--mui-palette-success-main)',
|
||||
'rgba(var(--mui-palette-success-mainChannel) / 0.8)',
|
||||
'rgba(var(--mui-palette-success-mainChannel) / 0.6)',
|
||||
'rgba(var(--mui-palette-success-mainChannel) / 0.4)'
|
||||
],
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
formatter(val: string) {
|
||||
return `${Number.parseInt(val)}%`
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'bottom',
|
||||
offsetY: 10,
|
||||
markers: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
offsetY: 1,
|
||||
offsetX: theme.direction === 'rtl' ? 8 : -4
|
||||
},
|
||||
itemMargin: {
|
||||
horizontal: 15,
|
||||
vertical: 5
|
||||
},
|
||||
fontSize: '13px',
|
||||
fontWeight: 400,
|
||||
labels: {
|
||||
colors: 'var()',
|
||||
useSeriesColors: false
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
padding: {
|
||||
top: 15
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '75%',
|
||||
labels: {
|
||||
show: true,
|
||||
value: {
|
||||
fontSize: '24px',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
fontWeight: 500,
|
||||
offsetY: -20
|
||||
},
|
||||
name: { offsetY: 20 },
|
||||
total: {
|
||||
show: true,
|
||||
fontSize: '0.9375rem',
|
||||
fontWeight: 400,
|
||||
label: 'AVG. Exceptions',
|
||||
color: 'var(--mui-palette-text-secondary)',
|
||||
formatter() {
|
||||
return '30%'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className='bs-full'>
|
||||
<CardHeader title='Delivery exceptions' action={<OptionMenu options={['Select All', 'Refresh', 'Share']} />} />
|
||||
<CardContent>
|
||||
<AppReactApexCharts
|
||||
type='donut'
|
||||
height={452}
|
||||
width='100%'
|
||||
series={deliveryExceptionsChartSeries}
|
||||
options={options}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogisticsDeliveryExceptions
|
||||
@@ -1,76 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Components Imports
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
|
||||
// Types Imports
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
type dataTypes = {
|
||||
title: string
|
||||
value: string
|
||||
change: number
|
||||
icon: string
|
||||
color: ThemeColor
|
||||
}
|
||||
|
||||
const deliveryData: dataTypes[] = [
|
||||
{ title: 'Packages in transit', value: '10k', change: 25.8, icon: 'tabler-box', color: 'primary' },
|
||||
{ title: 'Packages out for delivery', value: '5k', change: 4.3, icon: 'tabler-truck', color: 'info' },
|
||||
{ title: 'Packages delivered', value: '15k', change: -12.5, icon: 'tabler-circle-check', color: 'success' },
|
||||
{ title: 'Delivery success rate', value: '95%', change: 35.6, icon: 'tabler-percentage', color: 'warning' },
|
||||
{ title: 'Average delivery time', value: '2.5 Days', change: -2.15, icon: 'tabler-clock', color: 'secondary' },
|
||||
{ title: 'Customer satisfaction', value: '4.5/5', change: 5.7, icon: 'tabler-users', color: 'error' }
|
||||
]
|
||||
|
||||
const LogisticsDeliveryPerformance = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title='Delivery Performance'
|
||||
subheader='12% increase in this month'
|
||||
action={<OptionMenu options={['Select All', 'Refresh', 'Share']} />}
|
||||
/>
|
||||
<CardContent className='flex flex-col gap-[30px]'>
|
||||
{deliveryData.map((data, index) => (
|
||||
<div key={index} className='flex items-center gap-4'>
|
||||
<CustomAvatar skin='light' color={data.color} variant='rounded' size={38}>
|
||||
<i className={classnames(data.icon, 'text-[26px]')} />
|
||||
</CustomAvatar>
|
||||
<div className='flex justify-between items-center gap-4 is-full'>
|
||||
<div>
|
||||
<Typography color='text.primary' className='line-clamp-1'>
|
||||
{data.title}
|
||||
</Typography>
|
||||
<div className='flex items-center gap-1'>
|
||||
<i
|
||||
className={classnames(
|
||||
'text-xl',
|
||||
data.change > 0 ? 'tabler-chevron-up text-success' : 'tabler-chevron-down text-error'
|
||||
)}
|
||||
/>
|
||||
<Typography variant='body2' color={data.change > 0 ? 'success.main' : 'error.main'}>
|
||||
{data.change}%
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
{data.value}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogisticsDeliveryPerformance
|
||||
@@ -1,197 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { Fragment, useState } from 'react'
|
||||
import type { SyntheticEvent } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Divider from '@mui/material/Divider'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import Tab from '@mui/material/Tab'
|
||||
import TabList from '@mui/lab/TabList'
|
||||
import TabPanel from '@mui/lab/TabPanel'
|
||||
import TabContext from '@mui/lab/TabContext'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import TimelineDot from '@mui/lab/TimelineDot'
|
||||
import TimelineItem from '@mui/lab/TimelineItem'
|
||||
import TimelineContent from '@mui/lab/TimelineContent'
|
||||
import TimelineSeparator from '@mui/lab/TimelineSeparator'
|
||||
import TimelineConnector from '@mui/lab/TimelineConnector'
|
||||
import MuiTimeline from '@mui/lab/Timeline'
|
||||
import type { TimelineProps } from '@mui/lab/Timeline'
|
||||
|
||||
// Components Imports
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
type TimelineItemData = {
|
||||
name: string
|
||||
address: string
|
||||
}
|
||||
|
||||
type TimelineData = Record<'sender' | 'receiver', TimelineItemData>
|
||||
|
||||
type Data = Record<'new' | 'preparing' | 'shipping', TimelineData[]>
|
||||
|
||||
// Styled Timeline component
|
||||
const Timeline = styled(MuiTimeline)<TimelineProps>({
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
'& .MuiTimelineItem-root': {
|
||||
width: '100%',
|
||||
'&:before': {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
'& .MuiTimelineDot-root': {
|
||||
border: 0,
|
||||
padding: 0
|
||||
}
|
||||
})
|
||||
|
||||
// Vars
|
||||
const data: Data = {
|
||||
new: [
|
||||
{
|
||||
sender: {
|
||||
name: 'Micheal Hughes',
|
||||
address: '101 Boulder, California (CA), 933130'
|
||||
},
|
||||
receiver: {
|
||||
name: 'Daisy Coleman',
|
||||
address: '939 Orange, California (CA), 910614'
|
||||
}
|
||||
},
|
||||
{
|
||||
sender: {
|
||||
name: 'Glenn Todd',
|
||||
address: '1713 Garnet, California (CA), 939573'
|
||||
},
|
||||
receiver: {
|
||||
name: 'Arthur West',
|
||||
address: '156 Blaze, California (CA), 925878'
|
||||
}
|
||||
}
|
||||
],
|
||||
preparing: [
|
||||
{
|
||||
sender: {
|
||||
name: 'Rose Cole',
|
||||
address: '61 Unions, California (CA), 922523'
|
||||
},
|
||||
receiver: {
|
||||
name: 'Polly Spencer',
|
||||
address: '865 Delta, California (CA), 932830'
|
||||
}
|
||||
},
|
||||
{
|
||||
sender: {
|
||||
name: 'Jerry Wood',
|
||||
address: '37 Marjory, California (CA), 951958'
|
||||
},
|
||||
receiver: {
|
||||
name: 'Sam McCormick',
|
||||
address: '926 Reynolds, California (CA), 910279'
|
||||
}
|
||||
}
|
||||
],
|
||||
shipping: [
|
||||
{
|
||||
sender: {
|
||||
name: 'Alex Walton',
|
||||
address: '78 Judson, California (CA), 956084'
|
||||
},
|
||||
receiver: {
|
||||
name: 'Eula Griffin',
|
||||
address: '56 Bernard, California (CA), 965133'
|
||||
}
|
||||
},
|
||||
{
|
||||
sender: {
|
||||
name: 'Lula Barton',
|
||||
address: '95 Gaylord, California (CA), 991955'
|
||||
},
|
||||
receiver: {
|
||||
name: 'Craig Jacobs',
|
||||
address: '73 Sandy, California (CA), 954566'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const LogisticsOrdersByCountries = () => {
|
||||
// States
|
||||
const [value, setValue] = useState<string>('new')
|
||||
|
||||
const handleChange = (event: SyntheticEvent, newValue: string) => {
|
||||
setValue(newValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title='Orders by Countries'
|
||||
subheader='62 deliveries in progress'
|
||||
action={<OptionMenu options={['Show all orders', 'Share', 'Refresh']} />}
|
||||
className='pbe-4'
|
||||
/>
|
||||
<TabContext value={value}>
|
||||
<TabList variant='fullWidth' onChange={handleChange} aria-label='full width tabs example'>
|
||||
<Tab value='new' label='New' />
|
||||
<Tab value='preparing' label='Preparing' />
|
||||
<Tab value='shipping' label='Shipping' />
|
||||
</TabList>
|
||||
<TabPanel value={value} className='pbs-0'>
|
||||
<CardContent>
|
||||
{data[value as keyof Data].map((item: TimelineData, index: number) => {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<Timeline>
|
||||
<TimelineItem>
|
||||
<TimelineSeparator>
|
||||
<TimelineDot variant='outlined' className='mlb-0'>
|
||||
<i className='tabler-circle-check text-xl text-success' />
|
||||
</TimelineDot>
|
||||
<TimelineConnector />
|
||||
</TimelineSeparator>
|
||||
<TimelineContent className='flex flex-col gap-0.5 pbs-0 pis-5 pbe-5'>
|
||||
<Typography variant='body2' className='uppercase' color='success.main'>
|
||||
Sender
|
||||
</Typography>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
{item.sender.name}
|
||||
</Typography>
|
||||
<Typography className='line-clamp-1'>{item.sender.address}</Typography>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
<TimelineItem>
|
||||
<TimelineSeparator>
|
||||
<TimelineDot variant='outlined' className='mlb-0'>
|
||||
<i className='tabler-map-pin text-xl text-primary' />
|
||||
</TimelineDot>
|
||||
</TimelineSeparator>
|
||||
<TimelineContent className='flex flex-col pbe-0 gap-0.5 pbs-0 pis-5'>
|
||||
<Typography variant='body2' className='uppercase' color='primary.main'>
|
||||
Receiver
|
||||
</Typography>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
{item.receiver.name}
|
||||
</Typography>
|
||||
<Typography className='line-clamp-1'>{item.receiver.address}</Typography>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
{index !== data[value as keyof Data].length - 1 && <Divider className='mlb-4 border-dashed' />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogisticsOrdersByCountries
|
||||
@@ -1,272 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import LinearProgress from '@mui/material/LinearProgress'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Third-party Imports
|
||||
import type { RankingInfo } from '@tanstack/match-sorter-utils'
|
||||
import { rankItem } from '@tanstack/match-sorter-utils'
|
||||
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedMinMaxValues,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable
|
||||
} from '@tanstack/react-table'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { Vehicle } from '@/types/apps/logisticsTypes'
|
||||
import type { Locale } from '@configs/i18n'
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Components Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
// Util Imports
|
||||
import { getLocalizedUrl } from '@/utils/i18n'
|
||||
|
||||
// Style Imports
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface FilterFns {
|
||||
fuzzy: FilterFn<unknown>
|
||||
}
|
||||
interface FilterMeta {
|
||||
itemRank: RankingInfo
|
||||
}
|
||||
}
|
||||
|
||||
type chipColorType = {
|
||||
color: ThemeColor
|
||||
}
|
||||
|
||||
export const chipColor: { [key: string]: chipColorType } = {
|
||||
'No Warnings': { color: 'success' },
|
||||
'Fuel Problems': { color: 'primary' },
|
||||
'Temperature Not Optimal': { color: 'warning' },
|
||||
'Ecu Not Responding': { color: 'error' },
|
||||
'Oil Leakage': { color: 'info' }
|
||||
}
|
||||
|
||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value)
|
||||
|
||||
// Store the itemRank info
|
||||
addMeta({
|
||||
itemRank
|
||||
})
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed
|
||||
}
|
||||
|
||||
// Column Definitions
|
||||
const columnHelper = createColumnHelper<Vehicle>()
|
||||
|
||||
const LogisticsOverviewTable = ({ vehicleData }: { vehicleData?: Vehicle[] }) => {
|
||||
// States
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [data, setData] = useState(...[vehicleData])
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
const columns = useMemo<ColumnDef<Vehicle, any>[]>(
|
||||
() => [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
{...{
|
||||
checked: table.getIsAllRowsSelected(),
|
||||
indeterminate: table.getIsSomeRowsSelected(),
|
||||
onChange: table.getToggleAllRowsSelectedHandler()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
{...{
|
||||
checked: row.getIsSelected(),
|
||||
disabled: !row.getCanSelect(),
|
||||
indeterminate: row.getIsSomeSelected(),
|
||||
onChange: row.getToggleSelectedHandler()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
columnHelper.accessor('location', {
|
||||
header: 'Location',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center gap-4'>
|
||||
<CustomAvatar skin='light' color='secondary'>
|
||||
<i className='tabler-car text-[28px]' />
|
||||
</CustomAvatar>
|
||||
<Typography
|
||||
component={Link}
|
||||
href={getLocalizedUrl('/apps/logistics/fleet', locale as Locale)}
|
||||
className='font-medium hover:text-primary'
|
||||
color='text.primary'
|
||||
>
|
||||
VOL-{row.original.location}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('startCity', {
|
||||
header: 'Starting Route',
|
||||
cell: ({ row }) => <Typography>{`${row.original.startCity}, ${row.original.startCountry}`}</Typography>
|
||||
}),
|
||||
columnHelper.accessor('endCity', {
|
||||
header: 'Ending Route',
|
||||
cell: ({ row }) => <Typography>{`${row.original.endCity}, ${row.original.endCountry}`}</Typography>
|
||||
}),
|
||||
columnHelper.accessor('warnings', {
|
||||
header: 'Warnings',
|
||||
cell: ({ row }) => (
|
||||
<Chip
|
||||
variant='tonal'
|
||||
label={row.original.warnings}
|
||||
size='small'
|
||||
color={chipColor[row.original.warnings].color}
|
||||
/>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('progress', {
|
||||
header: 'Progress',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center gap-2 min-is-48'>
|
||||
<LinearProgress
|
||||
color='primary'
|
||||
value={row.original.progress}
|
||||
variant='determinate'
|
||||
className='bs-2 is-full'
|
||||
/>
|
||||
<Typography>{`${row.original.progress}%`}</Typography>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data: data as Vehicle[],
|
||||
columns,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter
|
||||
},
|
||||
state: {
|
||||
rowSelection
|
||||
},
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: 5
|
||||
}
|
||||
},
|
||||
enableRowSelection: true, //enable row selection for all rows
|
||||
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
getFacetedMinMaxValues: getFacetedMinMaxValues()
|
||||
})
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Course you are taking' action={<OptionMenu options={['Refresh', 'Update', 'Share']} />} />
|
||||
<div className='overflow-x-auto'>
|
||||
<table className={tableStyles.table}>
|
||||
<thead>
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<th key={header.id}>
|
||||
{header.isPlaceholder ? null : (
|
||||
<>
|
||||
<div
|
||||
className={classnames({
|
||||
'flex items-center': header.column.getIsSorted(),
|
||||
'cursor-pointer select-none': header.column.getCanSort()
|
||||
})}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{{
|
||||
asc: <i className='tabler-chevron-up text-xl' />,
|
||||
desc: <i className='tabler-chevron-down text-xl' />
|
||||
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
{table.getFilteredRowModel().rows.length === 0 ? (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
||||
No data available
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
) : (
|
||||
<tbody>
|
||||
{table
|
||||
.getRowModel()
|
||||
.rows.slice(0, table.getState().pagination.pageSize)
|
||||
.map(row => {
|
||||
return (
|
||||
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
{/* <TablePagination
|
||||
component={() => <TablePaginationComponent table={table as any} />}
|
||||
count={table.getFilteredRowModel().rows.length}
|
||||
rowsPerPage={table.getState().pagination.pageSize}
|
||||
page={table.getState().pagination.pageIndex}
|
||||
onPageChange={(_, page) => {
|
||||
table.setPageIndex(page)
|
||||
}}
|
||||
/> */}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogisticsOverviewTable
|
||||
@@ -1,238 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useRef, useState } from 'react'
|
||||
import type { SyntheticEvent } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
// Mui Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Grow from '@mui/material/Grow'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import Button from '@mui/material/Button'
|
||||
import Popper from '@mui/material/Popper'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import MenuList from '@mui/material/MenuList'
|
||||
import ButtonGroup from '@mui/material/ButtonGroup'
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Third Party Imports
|
||||
import type { ApexOptions } from 'apexcharts'
|
||||
|
||||
// Styled Component Imports
|
||||
const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts'))
|
||||
|
||||
// Style Imports
|
||||
import './styles.css'
|
||||
|
||||
const options = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
]
|
||||
|
||||
const MonthButton = () => {
|
||||
// States
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState<number>(0)
|
||||
|
||||
// Refs
|
||||
const anchorRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const handleMenuItemClick = (event: SyntheticEvent, index: number) => {
|
||||
setSelectedIndex(index)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen(prevOpen => !prevOpen)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup variant='tonal' ref={anchorRef} aria-label='split button' size='small'>
|
||||
<Button>{options[selectedIndex]}</Button>
|
||||
<Button
|
||||
className='pli-0 plb-[5px]'
|
||||
aria-haspopup='menu'
|
||||
onClick={handleToggle}
|
||||
aria-label='select merge strategy'
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
aria-controls={open ? 'split-button-menu' : undefined}
|
||||
>
|
||||
<i className='tabler-chevron-down text-xl' />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Popper open={open} anchorEl={anchorRef.current} role={undefined} transition placement='bottom-end'>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow {...TransitionProps} style={{ transformOrigin: placement === 'bottom-end' ? 'right top' : 'left top' }}>
|
||||
<Paper className='shadow-lg'>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList id='split-button-menu'>
|
||||
{options.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option}
|
||||
selected={index === selectedIndex}
|
||||
onClick={event => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: 'Shipment',
|
||||
type: 'column',
|
||||
data: [38, 45, 33, 38, 32, 48, 45, 40, 42, 37]
|
||||
},
|
||||
{
|
||||
name: 'Delivery',
|
||||
type: 'line',
|
||||
data: [23, 28, 23, 32, 25, 42, 32, 32, 26, 24]
|
||||
}
|
||||
]
|
||||
|
||||
const LogisticsShipmentStatistics = () => {
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
|
||||
const options: ApexOptions = {
|
||||
chart: {
|
||||
type: 'line',
|
||||
stacked: false,
|
||||
parentHeightOffset: 0,
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
markers: {
|
||||
size: 5,
|
||||
colors: '#fff',
|
||||
strokeColors: 'var(--mui-palette-primary-main)',
|
||||
hover: {
|
||||
size: 6
|
||||
},
|
||||
radius: 4
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: [0, 3],
|
||||
lineCap: 'round'
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'bottom',
|
||||
markers: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
offsetY: 1,
|
||||
offsetX: theme.direction === 'rtl' ? 8 : -4
|
||||
},
|
||||
height: 40,
|
||||
itemMargin: {
|
||||
horizontal: 10,
|
||||
vertical: 0
|
||||
},
|
||||
fontSize: '15px',
|
||||
fontFamily: 'Open Sans',
|
||||
fontWeight: 400,
|
||||
labels: {
|
||||
colors: 'var(--mui-palette-text-primary)'
|
||||
},
|
||||
offsetY: 10
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 8,
|
||||
borderColor: 'var(--mui-palette-divider)'
|
||||
},
|
||||
colors: ['var(--mui-palette-warning-main)', 'var(--mui-palette-primary-main)'],
|
||||
fill: {
|
||||
opacity: [1, 1]
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '30%',
|
||||
borderRadius: 4,
|
||||
borderRadiusApplication: 'end'
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
xaxis: {
|
||||
tickAmount: 10,
|
||||
categories: ['1 Jan', '2 Jan', '3 Jan', '4 Jan', '5 Jan', '6 Jan', '7 Jan', '8 Jan', '9 Jan', '10 Jan'],
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'var(--mui-palette-text-disabled)',
|
||||
fontSize: '13px',
|
||||
fontWeight: 400
|
||||
}
|
||||
},
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
axisTicks: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
tickAmount: 5,
|
||||
labels: {
|
||||
style: {
|
||||
colors: 'var(--mui-palette-text-disabled)',
|
||||
fontSize: '13px',
|
||||
fontWeight: 400
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Shipment Statistics' subheader='Total number of deliveries 23.8k' action={<MonthButton />} />
|
||||
<CardContent>
|
||||
<AppReactApexCharts
|
||||
id='shipment-statistics'
|
||||
type='line'
|
||||
height={310}
|
||||
width='100%'
|
||||
series={series}
|
||||
options={options}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogisticsShipmentStatistics
|
||||
@@ -1,24 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Types Imports
|
||||
import type { CardStatsHorizontalWithBorderProps } from '@/types/pages/widgetTypes'
|
||||
|
||||
// Components Imports
|
||||
import HorizontalWithBorder from '@components/card-statistics/HorizontalWithBorder'
|
||||
|
||||
const LogisticsStatisticsCard = ({ data }: { data?: CardStatsHorizontalWithBorderProps[] }) => {
|
||||
return (
|
||||
data && (
|
||||
<Grid container spacing={6}>
|
||||
{data.map((item, index) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }} key={index}>
|
||||
<HorizontalWithBorder {...item} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default LogisticsStatisticsCard
|
||||
@@ -1,140 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import LinearProgress from '@mui/material/LinearProgress'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Third-party Imports
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Components Imports
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
// Style Imports
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
import styles from './styles.module.css'
|
||||
|
||||
type dataTypes = {
|
||||
icon: string
|
||||
heading: string
|
||||
time: string
|
||||
progressColor: string
|
||||
progressColorVariant: string
|
||||
progressData: string
|
||||
widthClass?: string
|
||||
}
|
||||
|
||||
const data: dataTypes[] = [
|
||||
{
|
||||
icon: 'tabler-car',
|
||||
heading: 'On the way',
|
||||
time: '2hr 10min',
|
||||
progressColor: 'action',
|
||||
progressColorVariant: 'hover',
|
||||
progressData: '39.7%',
|
||||
widthClass: 'is-[39.7%]'
|
||||
},
|
||||
{
|
||||
icon: 'tabler-circle-arrow-down',
|
||||
heading: 'Unloading',
|
||||
time: '3hr 15min',
|
||||
progressColor: 'primary',
|
||||
progressColorVariant: 'main',
|
||||
progressData: '28.3%',
|
||||
widthClass: 'is-[28.3%]'
|
||||
},
|
||||
{
|
||||
icon: 'tabler-circle-arrow-up',
|
||||
heading: 'Loading',
|
||||
time: '1hr 24min',
|
||||
progressColor: 'info',
|
||||
progressColorVariant: 'main',
|
||||
progressData: '17.4%',
|
||||
widthClass: 'is-[17.4%]'
|
||||
},
|
||||
{
|
||||
icon: 'tabler-clock',
|
||||
heading: 'Waiting',
|
||||
time: '5hr 19min',
|
||||
progressColor: 'SnackbarContent',
|
||||
progressColorVariant: 'bg',
|
||||
progressData: '14.6%',
|
||||
widthClass: 'is-[14.6%]'
|
||||
}
|
||||
]
|
||||
|
||||
const LogisticsVehicleOverview = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Vehicle Overview' action={<OptionMenu options={['Refresh', 'Update', 'Share']} />} />
|
||||
<CardContent>
|
||||
<div className='flex flex-col gap-6'>
|
||||
<div className='flex is-full'>
|
||||
{data.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={classnames(item.widthClass, styles.linearRound, 'flex flex-col gap-[34px] relative')}
|
||||
>
|
||||
<Typography className={classnames(styles.header, 'relative max-sm:hidden')}>{item.heading}</Typography>
|
||||
<LinearProgress
|
||||
variant='determinate'
|
||||
value={-1}
|
||||
className={classnames('bs-[46px]')}
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
sx={{
|
||||
backgroundColor: `var(--mui-palette-${item.progressColor}-${item.progressColorVariant})`,
|
||||
borderRadius: 0
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant='body2'
|
||||
className='absolute bottom-3 start-2 font-medium'
|
||||
sx={{
|
||||
color: theme =>
|
||||
index === 0
|
||||
? 'var(--mui-palette-text-primary)'
|
||||
: item.progressColor === 'info'
|
||||
? 'var(--mui-palette-common-white)'
|
||||
: // eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
theme.palette.getContrastText(theme.palette[item.progressColor][item.progressColorVariant])
|
||||
}}
|
||||
>
|
||||
{item.progressData}
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className={tableStyles.table}>
|
||||
<tbody>
|
||||
{data.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className='flex items-center gap-2 pis-0'>
|
||||
<i className={classnames(item.icon, 'text-textPrimary text-[1.5rem]')} />
|
||||
<Typography color='text.primary'>{item.heading}</Typography>
|
||||
</td>
|
||||
<td className='text-end'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
{item.time}
|
||||
</Typography>
|
||||
</td>
|
||||
<td className='text-end pie-0'>
|
||||
<Typography>{item.progressData}</Typography>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogisticsVehicleOverview
|
||||
@@ -1,7 +0,0 @@
|
||||
#shipment-statistics .apexcharts-legend .apexcharts-legend-series {
|
||||
border: 1px solid var(--mui-palette-divider);
|
||||
border-radius: 0.375rem;
|
||||
block-size: 83%;
|
||||
padding-block: 4px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
.linearRound {
|
||||
&:first-child span {
|
||||
border-start-start-radius: 8px;
|
||||
border-end-start-radius: 8px;
|
||||
}
|
||||
&:last-child span {
|
||||
border-start-end-radius: 8px;
|
||||
border-end-end-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-block-end: -14px;
|
||||
inset-inline-start: 0;
|
||||
block-size: 10px;
|
||||
inline-size: 2px;
|
||||
background-color: var(--mui-palette-divider);
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// React Imports
|
||||
import { useRef, useEffect } from 'react'
|
||||
|
||||
// Third-party Imports
|
||||
import { Map, Marker } from 'react-map-gl'
|
||||
import type { MapRef } from 'react-map-gl'
|
||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||
|
||||
// Types Imports
|
||||
import type { viewStateType } from './index'
|
||||
|
||||
// Style Imports
|
||||
import './styles.css'
|
||||
|
||||
type Props = {
|
||||
viewState: viewStateType
|
||||
carIndex: number | false
|
||||
geojson: {
|
||||
type: string
|
||||
features: {
|
||||
type: string
|
||||
geometry: {
|
||||
type: string
|
||||
longitude: number
|
||||
latitude: number
|
||||
}
|
||||
}[]
|
||||
}
|
||||
mapboxAccessToken: string
|
||||
}
|
||||
|
||||
const FleetMap = (props: Props) => {
|
||||
// Vars
|
||||
const { carIndex, viewState, geojson, mapboxAccessToken } = props
|
||||
|
||||
// Hooks
|
||||
const mapRef = useRef<MapRef>()
|
||||
|
||||
useEffect(() => {
|
||||
mapRef.current?.flyTo({ center: [viewState.longitude, viewState.latitude], zoom: 16 })
|
||||
}, [viewState])
|
||||
|
||||
return (
|
||||
<div className='is-full bs-full'>
|
||||
<Map
|
||||
mapboxAccessToken={mapboxAccessToken}
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
ref={mapRef}
|
||||
initialViewState={{ longitude: -73.999024, latitude: 40.75249842, zoom: 12.5 }}
|
||||
mapStyle='mapbox://styles/mapbox/light-v9'
|
||||
attributionControl={false}
|
||||
>
|
||||
{geojson.features.map((item, index) => {
|
||||
return (
|
||||
<Marker
|
||||
key={index}
|
||||
longitude={item.geometry.longitude}
|
||||
latitude={item.geometry.latitude}
|
||||
style={{ display: 'flex' }}
|
||||
>
|
||||
<img
|
||||
src='/images/apps/logistics/fleet-car.png'
|
||||
height={42}
|
||||
width={20}
|
||||
{...(index === carIndex && {
|
||||
style: { filter: 'drop-shadow(0 0 7px var(--mui-palette-primary-main))' }
|
||||
})}
|
||||
/>
|
||||
</Marker>
|
||||
)
|
||||
})}
|
||||
</Map>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FleetMap
|
||||
@@ -1,321 +0,0 @@
|
||||
// React Imports
|
||||
import { useEffect } from 'react'
|
||||
import type { ReactNode, SyntheticEvent } from 'react'
|
||||
|
||||
// Mui Imports
|
||||
import MuiAccordion from '@mui/material/Accordion'
|
||||
import MuiAccordionDetails from '@mui/material/AccordionDetails'
|
||||
import MuiAccordionSummary from '@mui/material/AccordionSummary'
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import LinearProgress from '@mui/material/LinearProgress'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import MuiTimeline from '@mui/lab/Timeline'
|
||||
import TimelineItem from '@mui/lab/TimelineItem'
|
||||
import TimelineSeparator from '@mui/lab/TimelineSeparator'
|
||||
import TimelineDot from '@mui/lab/TimelineDot'
|
||||
import TimelineConnector from '@mui/lab/TimelineConnector'
|
||||
import TimelineContent from '@mui/lab/TimelineContent'
|
||||
import type { AccordionProps } from '@mui/material/Accordion'
|
||||
import type { AccordionSummaryProps } from '@mui/material/AccordionSummary'
|
||||
import type { AccordionDetailsProps } from '@mui/material/AccordionDetails'
|
||||
import type { TimelineProps } from '@mui/lab/Timeline'
|
||||
|
||||
// Third-party Imports
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
|
||||
// Types Imports
|
||||
import type { viewStateType } from './index'
|
||||
|
||||
// Components Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
|
||||
type Props = {
|
||||
backdropOpen: boolean
|
||||
setBackdropOpen: (value: boolean) => void
|
||||
sidebarOpen: boolean
|
||||
setSidebarOpen: (value: boolean) => void
|
||||
isBelowLgScreen: boolean
|
||||
isBelowMdScreen: boolean
|
||||
isBelowSmScreen: boolean
|
||||
expanded: number | false
|
||||
setExpanded: (value: number | false) => void
|
||||
setViewState: (value: viewStateType) => void
|
||||
geojson: {
|
||||
type: string
|
||||
features: {
|
||||
type: string
|
||||
geometry: {
|
||||
type: string
|
||||
longitude: number
|
||||
latitude: number
|
||||
}
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
// Styled component for Accordion component
|
||||
const Accordion = styled(MuiAccordion)<AccordionProps>({
|
||||
boxShadow: 'none !important',
|
||||
border: 'none',
|
||||
'&:before': {
|
||||
content: 'none'
|
||||
},
|
||||
marginBlockEnd: '0px !important'
|
||||
})
|
||||
|
||||
// Styled component for AccordionSummary component
|
||||
const AccordionSummary = styled(MuiAccordionSummary)<AccordionSummaryProps>(({ theme }) => ({
|
||||
paddingBlock: theme.spacing(0, 6),
|
||||
paddingInline: theme.spacing(0)
|
||||
}))
|
||||
|
||||
// Styled component for AccordionDetails component
|
||||
const AccordionDetails = styled(MuiAccordionDetails)<AccordionDetailsProps>(({ theme }) => ({
|
||||
paddingBlock: theme.spacing(0, 1),
|
||||
paddingInline: theme.spacing(0)
|
||||
}))
|
||||
|
||||
// Styled Timeline component
|
||||
const Timeline = styled(MuiTimeline)<TimelineProps>({
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
'& .MuiTimelineItem-root': {
|
||||
width: '100%',
|
||||
'&:before': {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
'& .MuiTimelineDot-root': {
|
||||
border: 0,
|
||||
padding: 0
|
||||
}
|
||||
})
|
||||
|
||||
type VehicleTrackingDataType = {
|
||||
name: string
|
||||
location: string
|
||||
progress: number
|
||||
driverName: string
|
||||
passengerName: string
|
||||
}
|
||||
|
||||
const vehicleTrackingData: VehicleTrackingDataType[] = [
|
||||
{
|
||||
name: 'VOL-342808',
|
||||
location: 'Chelsea, NY, USA',
|
||||
progress: 88,
|
||||
driverName: 'Veronica Herman',
|
||||
passengerName: 'Helen Jacobs'
|
||||
},
|
||||
{
|
||||
name: 'VOL-954784',
|
||||
location: 'Lincoln Harbor, NY, USA',
|
||||
progress: 90,
|
||||
driverName: 'Myrtle Ullrich',
|
||||
passengerName: 'William Miller'
|
||||
},
|
||||
{
|
||||
name: 'VOL-342808',
|
||||
location: 'Midtown East, NY, USA',
|
||||
progress: 60,
|
||||
driverName: 'Barry Schowalter',
|
||||
passengerName: 'Charles Anderson'
|
||||
},
|
||||
{
|
||||
name: 'VOL-343908',
|
||||
location: 'Hoboken, NY, USA',
|
||||
progress: 28,
|
||||
driverName: 'Frank Jones',
|
||||
passengerName: 'Edward Smith'
|
||||
}
|
||||
]
|
||||
|
||||
const ScrollWrapper = ({ children, isBelowLgScreen }: { children: ReactNode; isBelowLgScreen: boolean }) => {
|
||||
if (isBelowLgScreen) {
|
||||
return <div className='bs-full overflow-y-auto overflow-x-hidden pbe-6 pli-6'>{children}</div>
|
||||
} else {
|
||||
return (
|
||||
<PerfectScrollbar options={{ wheelPropagation: false, suppressScrollX: true }} className='pbe-6 pli-6'>
|
||||
{children}
|
||||
</PerfectScrollbar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const VehicleTracking = ({
|
||||
vehicleTrackingData,
|
||||
index,
|
||||
expanded,
|
||||
handleChange
|
||||
}: {
|
||||
vehicleTrackingData: VehicleTrackingDataType
|
||||
index: number
|
||||
expanded: number | false
|
||||
handleChange: (panel: number) => (event: SyntheticEvent, isExpanded: boolean) => void
|
||||
}) => {
|
||||
return (
|
||||
<Accordion expanded={expanded === index} onChange={handleChange(index)}>
|
||||
<AccordionSummary>
|
||||
<div className='flex gap-4 items-center'>
|
||||
<CustomAvatar skin='light' color='secondary'>
|
||||
<i className='tabler-car' />
|
||||
</CustomAvatar>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography className='font-normal'>{vehicleTrackingData.name}</Typography>
|
||||
<Typography className='font-normal !text-textSecondary'>{vehicleTrackingData.location}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<div className='flex flex-col gap-1 plb-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography className='!text-textPrimary'>Delivery Process</Typography>
|
||||
<Typography>{vehicleTrackingData.progress}%</Typography>
|
||||
</div>
|
||||
<LinearProgress variant='determinate' value={vehicleTrackingData.progress} />
|
||||
</div>
|
||||
<Timeline className='pbs-4'>
|
||||
<TimelineItem>
|
||||
<TimelineSeparator>
|
||||
<TimelineDot variant='outlined' className='mlb-0'>
|
||||
<i className='tabler-circle-check text-xl text-success' />
|
||||
</TimelineDot>
|
||||
<TimelineConnector />
|
||||
</TimelineSeparator>
|
||||
<TimelineContent className='flex flex-col gap-0.5 pbs-0 pis-4 pbe-5'>
|
||||
<Typography variant='caption' className='uppercase !text-success'>
|
||||
Tracking Number Created
|
||||
</Typography>
|
||||
<Typography className='font-medium !text-textPrimary'>{vehicleTrackingData.driverName}</Typography>
|
||||
<Typography variant='body2'>Sep 01, 7:53 AM</Typography>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
<Timeline>
|
||||
<TimelineItem>
|
||||
<TimelineSeparator>
|
||||
<TimelineDot variant='outlined' className='mlb-0'>
|
||||
<i className='tabler-circle-check text-xl text-success' />
|
||||
</TimelineDot>
|
||||
<TimelineConnector />
|
||||
</TimelineSeparator>
|
||||
<TimelineContent className='flex flex-col gap-0.5 pbs-0 pis-4 pbe-5'>
|
||||
<Typography variant='caption' className='uppercase !text-success'>
|
||||
Out For Delivery
|
||||
</Typography>
|
||||
<Typography className='font-medium !text-textPrimary'>{vehicleTrackingData.driverName}</Typography>
|
||||
<Typography variant='body2'>Sep 03, 8:02 AM</Typography>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
<Timeline>
|
||||
<TimelineItem>
|
||||
<TimelineSeparator>
|
||||
<TimelineDot variant='outlined' className='mlb-0'>
|
||||
<i className='tabler-map-pin text-xl text-primary' />
|
||||
</TimelineDot>
|
||||
</TimelineSeparator>
|
||||
<TimelineContent className='flex flex-col gap-0.5 pbs-0 pis-4 pbe-5'>
|
||||
<Typography variant='caption' className='uppercase !text-primary'>
|
||||
Arrived
|
||||
</Typography>
|
||||
<Typography className='font-medium !text-textPrimary'>{vehicleTrackingData.passengerName}</Typography>
|
||||
<Typography variant='body2'>Sep 03, 8:02 AM</Typography>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
</Timeline>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
||||
const FleetSidebar = (props: Props) => {
|
||||
// Props
|
||||
const {
|
||||
backdropOpen,
|
||||
setBackdropOpen,
|
||||
sidebarOpen,
|
||||
setSidebarOpen,
|
||||
isBelowLgScreen,
|
||||
isBelowMdScreen,
|
||||
isBelowSmScreen,
|
||||
expanded,
|
||||
setExpanded,
|
||||
setViewState,
|
||||
geojson
|
||||
} = props
|
||||
|
||||
const handleChange = (panel: number) => (event: SyntheticEvent, isExpanded: boolean) => {
|
||||
if (isExpanded) {
|
||||
setViewState({
|
||||
longitude: geojson.features[panel].geometry.longitude,
|
||||
latitude: geojson.features[panel].geometry.latitude,
|
||||
zoom: 16
|
||||
})
|
||||
}
|
||||
|
||||
setExpanded(isExpanded ? panel : false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!backdropOpen && sidebarOpen) {
|
||||
setSidebarOpen(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [backdropOpen])
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className='bs-full'
|
||||
open={sidebarOpen}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
variant={!isBelowMdScreen ? 'permanent' : 'persistent'}
|
||||
ModalProps={{
|
||||
disablePortal: true,
|
||||
keepMounted: true // Better open performance on mobile.
|
||||
}}
|
||||
sx={{
|
||||
zIndex: isBelowMdScreen && sidebarOpen ? 11 : 10,
|
||||
position: !isBelowMdScreen ? 'static' : 'absolute',
|
||||
...(isBelowSmScreen && sidebarOpen && { width: '100%' }),
|
||||
'& .MuiDrawer-paper': {
|
||||
borderRight: 'none',
|
||||
boxShadow: 'none',
|
||||
overflow: 'hidden',
|
||||
width: isBelowSmScreen ? '100%' : '360px',
|
||||
position: !isBelowMdScreen ? 'static' : 'absolute'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='flex justify-between items-center p-6'>
|
||||
<Typography variant='h5'>Fleet</Typography>
|
||||
|
||||
{isBelowMdScreen ? (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setSidebarOpen(false)
|
||||
setBackdropOpen(false)
|
||||
}}
|
||||
>
|
||||
<i className='tabler-x' />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</div>
|
||||
<ScrollWrapper isBelowLgScreen={isBelowLgScreen}>
|
||||
{vehicleTrackingData.map((item, index) => (
|
||||
<VehicleTracking
|
||||
vehicleTrackingData={item}
|
||||
index={index}
|
||||
expanded={expanded}
|
||||
handleChange={handleChange}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</ScrollWrapper>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default FleetSidebar
|
||||
@@ -1,144 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Backdrop from '@mui/material/Backdrop'
|
||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||
import type { Theme } from '@mui/material/styles'
|
||||
|
||||
// Third-party Imports
|
||||
import classNames from 'classnames'
|
||||
|
||||
//Components Imports
|
||||
import CustomIconButton from '@core/components/mui/IconButton'
|
||||
import FleetSidebar from './FleetSidebar'
|
||||
import FleetMap from './FleetMap'
|
||||
|
||||
// Hook Imports
|
||||
import { useSettings } from '@core/hooks/useSettings'
|
||||
|
||||
// Util Imports
|
||||
import { commonLayoutClasses } from '@layouts/utils/layoutClasses'
|
||||
|
||||
export type viewStateType = {
|
||||
longitude: number
|
||||
latitude: number
|
||||
zoom: number
|
||||
}
|
||||
|
||||
const geojson = {
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
longitude: -73.999024,
|
||||
latitude: 40.75249842
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
longitude: -74.03,
|
||||
latitude: 40.75699842
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
longitude: -73.967524,
|
||||
latitude: 40.7599842
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
longitude: -74.0325,
|
||||
latitude: 40.742992
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const Fleet = ({ mapboxAccessToken }: { mapboxAccessToken: string }) => {
|
||||
// States
|
||||
const [backdropOpen, setBackdropOpen] = useState(false)
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const [expanded, setExpanded] = useState<number | false>(0)
|
||||
|
||||
const [viewState, setViewState] = useState<viewStateType>({
|
||||
longitude: -73.999024,
|
||||
latitude: 40.75249842,
|
||||
zoom: 12.5
|
||||
})
|
||||
|
||||
// Hooks
|
||||
const { settings } = useSettings()
|
||||
const isBelowLgScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('lg'))
|
||||
const isBelowMdScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'))
|
||||
const isBelowSmScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBelowMdScreen && backdropOpen && sidebarOpen) {
|
||||
setBackdropOpen(false)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isBelowMdScreen])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBelowSmScreen && sidebarOpen) {
|
||||
setBackdropOpen(true)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isBelowSmScreen])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
commonLayoutClasses.contentHeightFixed,
|
||||
'flex is-full overflow-hidden rounded-xl relative',
|
||||
{
|
||||
border: settings.skin === 'bordered',
|
||||
'shadow-md': settings.skin !== 'bordered'
|
||||
}
|
||||
)}
|
||||
>
|
||||
{isBelowMdScreen ? (
|
||||
<CustomIconButton
|
||||
variant='contained'
|
||||
color='primary'
|
||||
className='absolute top-4 left-4 z-10 bg-backgroundPaper text-textPrimary shadow-xs shadow-gray-500 hover:bg-backgroundPaper focus:bg-backgroundPaper active:bg-backgroundPaper'
|
||||
onClick={() => {
|
||||
setSidebarOpen(true)
|
||||
setBackdropOpen(true)
|
||||
}}
|
||||
>
|
||||
<i className='tabler-menu-2' />
|
||||
</CustomIconButton>
|
||||
) : null}
|
||||
<FleetSidebar
|
||||
backdropOpen={backdropOpen}
|
||||
setBackdropOpen={setBackdropOpen}
|
||||
sidebarOpen={sidebarOpen}
|
||||
setSidebarOpen={setSidebarOpen}
|
||||
isBelowMdScreen={isBelowMdScreen}
|
||||
isBelowLgScreen={isBelowLgScreen}
|
||||
isBelowSmScreen={isBelowSmScreen}
|
||||
expanded={expanded}
|
||||
setExpanded={setExpanded}
|
||||
setViewState={setViewState}
|
||||
geojson={geojson}
|
||||
/>
|
||||
<FleetMap carIndex={expanded} viewState={viewState} geojson={geojson} mapboxAccessToken={mapboxAccessToken} />
|
||||
<Backdrop open={backdropOpen} onClick={() => setBackdropOpen(false)} className='absolute z-10' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Fleet
|
||||
@@ -1,3 +0,0 @@
|
||||
.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl a.mapboxgl-ctrl-logo {
|
||||
display: none;
|
||||
}
|
||||
@@ -6,9 +6,6 @@ import ProjectListTable from './ProjectListTable'
|
||||
import UserActivityTimeLine from './UserActivityTimeline'
|
||||
import InvoiceListTable from './InvoiceListTable'
|
||||
|
||||
// Data Imports
|
||||
import { getInvoiceData } from '@/app/server/actions'
|
||||
|
||||
/**
|
||||
* ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the
|
||||
* ! `.env` file found at root of your project and also update the API endpoints like `/apps/invoice` in below example.
|
||||
@@ -27,8 +24,6 @@ import { getInvoiceData } from '@/app/server/actions'
|
||||
} */
|
||||
|
||||
const OverViewTab = async () => {
|
||||
// Vars
|
||||
const invoiceData = await getInvoiceData()
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
@@ -39,7 +34,7 @@ const OverViewTab = async () => {
|
||||
<UserActivityTimeLine />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<InvoiceListTable invoiceData={invoiceData} />
|
||||
<InvoiceListTable invoiceData={[]} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import Button from '@mui/material/Button'
|
||||
import FormControl from '@mui/material/FormControl'
|
||||
import FormHelperText from '@mui/material/FormHelperText'
|
||||
|
||||
// Third-party Imports
|
||||
import { useForm, Controller } from 'react-hook-form'
|
||||
|
||||
// Component Imports
|
||||
import ConfirmationDialog from '@components/dialogs/confirmation-dialog'
|
||||
|
||||
const AccountDelete = () => {
|
||||
// States
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm({ defaultValues: { checkbox: false } })
|
||||
|
||||
// Vars
|
||||
const checkboxValue = watch('checkbox')
|
||||
|
||||
const onSubmit = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Delete Account' />
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormControl error={Boolean(errors.checkbox)} className='is-full mbe-6'>
|
||||
<Controller
|
||||
name='checkbox'
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<FormControlLabel control={<Checkbox {...field} />} label='I confirm my account deactivation' />
|
||||
)}
|
||||
/>
|
||||
{errors.checkbox && <FormHelperText error>Please confirm you want to delete account</FormHelperText>}
|
||||
</FormControl>
|
||||
<Button variant='contained' color='error' type='submit' disabled={!checkboxValue}>
|
||||
Deactivate Account
|
||||
</Button>
|
||||
<ConfirmationDialog open={open} setOpen={setOpen} type='delete-account' />
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountDelete
|
||||
@@ -1,300 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { ChangeEvent } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Button from '@mui/material/Button'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import type { SelectChangeEvent } from '@mui/material/Select'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
type Data = {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
organization: string
|
||||
phoneNumber: number | string
|
||||
address: string
|
||||
state: string
|
||||
zipCode: string
|
||||
country: string
|
||||
language: string
|
||||
timezone: string
|
||||
currency: string
|
||||
}
|
||||
|
||||
// Vars
|
||||
const initialData: Data = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@example.com',
|
||||
organization: 'Pixinvent',
|
||||
phoneNumber: '+1 (917) 543-9876',
|
||||
address: '123 Main St, New York, NY 10001',
|
||||
state: 'New York',
|
||||
zipCode: '634880',
|
||||
country: 'usa',
|
||||
language: 'english',
|
||||
timezone: 'gmt-12',
|
||||
currency: 'usd'
|
||||
}
|
||||
|
||||
const languageData = ['English', 'Arabic', 'French', 'German', 'Portuguese']
|
||||
|
||||
const AccountDetails = () => {
|
||||
// States
|
||||
const [formData, setFormData] = useState<Data>(initialData)
|
||||
const [fileInput, setFileInput] = useState<string>('')
|
||||
const [imgSrc, setImgSrc] = useState<string>('/images/avatars/1.png')
|
||||
const [language, setLanguage] = useState<string[]>(['English'])
|
||||
|
||||
const handleDelete = (value: string) => {
|
||||
setLanguage(current => current.filter(item => item !== value))
|
||||
}
|
||||
|
||||
const handleChange = (event: SelectChangeEvent<string[]>) => {
|
||||
setLanguage(event.target.value as string[])
|
||||
}
|
||||
|
||||
const handleFormChange = (field: keyof Data, value: Data[keyof Data]) => {
|
||||
setFormData({ ...formData, [field]: value })
|
||||
}
|
||||
|
||||
const handleFileInputChange = (file: ChangeEvent) => {
|
||||
const reader = new FileReader()
|
||||
const { files } = file.target as HTMLInputElement
|
||||
|
||||
if (files && files.length !== 0) {
|
||||
reader.onload = () => setImgSrc(reader.result as string)
|
||||
reader.readAsDataURL(files[0])
|
||||
|
||||
if (reader.result !== null) {
|
||||
setFileInput(reader.result as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileInputReset = () => {
|
||||
setFileInput('')
|
||||
setImgSrc('/images/avatars/1.png')
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className='mbe-4'>
|
||||
<div className='flex max-sm:flex-col items-center gap-6'>
|
||||
<img height={100} width={100} className='rounded' src={imgSrc} alt='Profile' />
|
||||
<div className='flex flex-grow flex-col gap-4'>
|
||||
<div className='flex flex-col sm:flex-row gap-4'>
|
||||
<Button component='label' variant='contained' htmlFor='account-settings-upload-image'>
|
||||
Upload New Photo
|
||||
<input
|
||||
hidden
|
||||
type='file'
|
||||
value={fileInput}
|
||||
accept='image/png, image/jpeg'
|
||||
onChange={handleFileInputChange}
|
||||
id='account-settings-upload-image'
|
||||
/>
|
||||
</Button>
|
||||
<Button variant='tonal' color='secondary' onClick={handleFileInputReset}>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
<Typography>Allowed JPG, GIF or PNG. Max size of 800K</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<form onSubmit={e => e.preventDefault()}>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='First Name'
|
||||
value={formData.firstName}
|
||||
placeholder='John'
|
||||
onChange={e => handleFormChange('firstName', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Last Name'
|
||||
value={formData.lastName}
|
||||
placeholder='Doe'
|
||||
onChange={e => handleFormChange('lastName', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Email'
|
||||
value={formData.email}
|
||||
placeholder='john.doe@gmail.com'
|
||||
onChange={e => handleFormChange('email', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Organization'
|
||||
value={formData.organization}
|
||||
placeholder='Pixinvent'
|
||||
onChange={e => handleFormChange('organization', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Phone Number'
|
||||
value={formData.phoneNumber}
|
||||
placeholder='+1 (234) 567-8901'
|
||||
onChange={e => handleFormChange('phoneNumber', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='Address'
|
||||
value={formData.address}
|
||||
placeholder='Address'
|
||||
onChange={e => handleFormChange('address', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label='State'
|
||||
value={formData.state}
|
||||
placeholder='New York'
|
||||
onChange={e => handleFormChange('state', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
type='number'
|
||||
label='Zip Code'
|
||||
value={formData.zipCode}
|
||||
placeholder='123456'
|
||||
onChange={e => handleFormChange('zipCode', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
select
|
||||
fullWidth
|
||||
label='Country'
|
||||
value={formData.country}
|
||||
onChange={e => handleFormChange('country', e.target.value)}
|
||||
>
|
||||
<MenuItem value='usa'>USA</MenuItem>
|
||||
<MenuItem value='uk'>UK</MenuItem>
|
||||
<MenuItem value='australia'>Australia</MenuItem>
|
||||
<MenuItem value='germany'>Germany</MenuItem>
|
||||
</CustomTextField>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
select
|
||||
fullWidth
|
||||
label='Language'
|
||||
value={language}
|
||||
slotProps={{
|
||||
select: {
|
||||
multiple: true, // @ts-ignore
|
||||
onChange: handleChange,
|
||||
renderValue: selected => (
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{(selected as string[]).map(value => (
|
||||
<Chip
|
||||
key={value}
|
||||
clickable
|
||||
onMouseDown={event => event.stopPropagation()}
|
||||
size='small'
|
||||
label={value}
|
||||
onDelete={() => handleDelete(value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{languageData.map(name => (
|
||||
<MenuItem key={name} value={name}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</CustomTextField>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
select
|
||||
fullWidth
|
||||
label='TimeZone'
|
||||
value={formData.timezone}
|
||||
onChange={e => handleFormChange('timezone', e.target.value)}
|
||||
slotProps={{
|
||||
select: { MenuProps: { PaperProps: { style: { maxHeight: 250 } } } }
|
||||
}}
|
||||
>
|
||||
<MenuItem value='gmt-12'>(GMT-12:00) International Date Line West</MenuItem>
|
||||
<MenuItem value='gmt-11'>(GMT-11:00) Midway Island, Samoa</MenuItem>
|
||||
<MenuItem value='gmt-10'>(GMT-10:00) Hawaii</MenuItem>
|
||||
<MenuItem value='gmt-09'>(GMT-09:00) Alaska</MenuItem>
|
||||
<MenuItem value='gmt-08'>(GMT-08:00) Pacific Time (US & Canada)</MenuItem>
|
||||
<MenuItem value='gmt-08-baja'>(GMT-08:00) Tijuana, Baja California</MenuItem>
|
||||
<MenuItem value='gmt-07'>(GMT-07:00) Chihuahua, La Paz, Mazatlan</MenuItem>
|
||||
<MenuItem value='gmt-07-mt'>(GMT-07:00) Mountain Time (US & Canada)</MenuItem>
|
||||
<MenuItem value='gmt-06'>(GMT-06:00) Central America</MenuItem>
|
||||
<MenuItem value='gmt-06-ct'>(GMT-06:00) Central Time (US & Canada)</MenuItem>
|
||||
<MenuItem value='gmt-06-mc'>(GMT-06:00) Guadalajara, Mexico City, Monterrey</MenuItem>
|
||||
<MenuItem value='gmt-06-sk'>(GMT-06:00) Saskatchewan</MenuItem>
|
||||
<MenuItem value='gmt-05'>(GMT-05:00) Bogota, Lima, Quito, Rio Branco</MenuItem>
|
||||
<MenuItem value='gmt-05-et'>(GMT-05:00) Eastern Time (US & Canada)</MenuItem>
|
||||
<MenuItem value='gmt-05-ind'>(GMT-05:00) Indiana (East)</MenuItem>
|
||||
<MenuItem value='gmt-04'>(GMT-04:00) Atlantic Time (Canada)</MenuItem>
|
||||
<MenuItem value='gmt-04-clp'>(GMT-04:00) Caracas, La Paz</MenuItem>
|
||||
</CustomTextField>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
select
|
||||
fullWidth
|
||||
label='Currency'
|
||||
value={formData.currency}
|
||||
onChange={e => handleFormChange('currency', e.target.value)}
|
||||
>
|
||||
<MenuItem value='usd'>USD</MenuItem>
|
||||
<MenuItem value='euro'>EUR</MenuItem>
|
||||
<MenuItem value='pound'>Pound</MenuItem>
|
||||
<MenuItem value='bitcoin'>Bitcoin</MenuItem>
|
||||
</CustomTextField>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }} className='flex gap-4 flex-wrap'>
|
||||
<Button variant='contained' type='submit'>
|
||||
Save Changes
|
||||
</Button>
|
||||
<Button variant='tonal' type='reset' color='secondary' onClick={() => setFormData(initialData)}>
|
||||
Reset
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountDetails
|
||||
@@ -1,21 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import AccountDetails from './AccountDetails'
|
||||
import AccountDelete from './AccountDelete'
|
||||
|
||||
const Account = () => {
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<AccountDetails />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<AccountDelete />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default Account
|
||||
@@ -1,85 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Button from '@mui/material/Button'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import InputAdornment from '@mui/material/InputAdornment'
|
||||
|
||||
// Component Imports
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
const Address = () => {
|
||||
// States
|
||||
const [state, setState] = useState('')
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Billing Address' />
|
||||
<CardContent>
|
||||
<form>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField fullWidth label='Company Name' variant='outlined' placeholder='Pixinvent' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField fullWidth label='Billing Email' variant='outlined' placeholder='john.doe@example.com' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField fullWidth label='TAX ID' variant='outlined' placeholder='Enter TAX ID' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField fullWidth label='VAT Number' variant='outlined' placeholder='Enter VAT Number' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
type='number'
|
||||
label='Mobile Number'
|
||||
placeholder='202 555 0111'
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: <InputAdornment position='start'>US (+1)</InputAdornment>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField select fullWidth label='Country' value={state} onChange={e => setState(e.target.value)}>
|
||||
<MenuItem value=''>Select Country</MenuItem>
|
||||
<MenuItem value='australia'>Australia</MenuItem>
|
||||
<MenuItem value='canada'>Canada</MenuItem>
|
||||
<MenuItem value='france'>France</MenuItem>
|
||||
<MenuItem value='united-kingdom'>United Kingdom</MenuItem>
|
||||
<MenuItem value='united-states'>United States</MenuItem>
|
||||
</CustomTextField>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CustomTextField fullWidth label='Billing Address' variant='outlined' placeholder='Billing Address' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField fullWidth label='State' variant='outlined' placeholder='California' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<CustomTextField fullWidth type='number' label='Zip Code' variant='outlined' placeholder='231465' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }} className='flex gap-4 flex-wrap'>
|
||||
<Button variant='contained'>Save Changes</Button>
|
||||
<Button variant='tonal' type='reset' color='secondary' onClick={() => setState('')}>
|
||||
Discard
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default Address
|
||||
@@ -1,96 +0,0 @@
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Button from '@mui/material/Button'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import AlertTitle from '@mui/material/AlertTitle'
|
||||
import LinearProgress from '@mui/material/LinearProgress'
|
||||
import type { ButtonProps } from '@mui/material/Button'
|
||||
|
||||
// Type Imports
|
||||
import type { PricingPlanType } from '@/types/pages/pricingTypes'
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Component Imports
|
||||
import ConfirmationDialog from '@components/dialogs/confirmation-dialog'
|
||||
import UpgradePlan from '@components/dialogs/upgrade-plan'
|
||||
import OpenDialogOnElementClick from '@components/dialogs/OpenDialogOnElementClick'
|
||||
|
||||
const CurrentPlan = ({ data }: { data?: PricingPlanType[] }) => {
|
||||
const buttonProps = (children: string, color: ThemeColor, variant: ButtonProps['variant']): ButtonProps => ({
|
||||
children,
|
||||
variant,
|
||||
color
|
||||
})
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Current Plan' />
|
||||
<CardContent>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, md: 6 }} className='flex flex-col gap-6'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
Your Current Plan is Basic
|
||||
</Typography>
|
||||
<Typography>A simple start for everyone</Typography>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
Active until Dec 09, 2021
|
||||
</Typography>
|
||||
<Typography>We will send you a notification upon Subscription expiration</Typography>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
$199 Per Month
|
||||
</Typography>
|
||||
<Chip color='primary' variant='tonal' label='Popular' size='small' />
|
||||
</div>
|
||||
<Typography>Standard plan for small to medium businesses</Typography>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }} className='flex flex-col gap-6'>
|
||||
<Alert severity='warning'>
|
||||
<AlertTitle>We need your attention!</AlertTitle>
|
||||
Your plan requires update
|
||||
</Alert>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
Days
|
||||
</Typography>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
12 of 30 Days
|
||||
</Typography>
|
||||
</div>
|
||||
<LinearProgress variant='determinate' value={20} />
|
||||
<Typography variant='body2'>18 days remaining until your plan requires update</Typography>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }} className='flex gap-4 flex-wrap'>
|
||||
<OpenDialogOnElementClick
|
||||
element={Button}
|
||||
elementProps={buttonProps('Upgrade Plan', 'primary', 'contained')}
|
||||
dialog={UpgradePlan}
|
||||
dialogProps={{ data: data }}
|
||||
/>
|
||||
<OpenDialogOnElementClick
|
||||
element={Button}
|
||||
elementProps={buttonProps('Cancel Subscription', 'error', 'tonal')}
|
||||
dialog={ConfirmationDialog}
|
||||
dialogProps={{ type: 'unsubscribe' }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default CurrentPlan
|
||||
@@ -1,461 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
|
||||
// MUI Imports
|
||||
import Button from '@mui/material/Button'
|
||||
import Card from '@mui/material/Card'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import type { TextFieldProps } from '@mui/material/TextField'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
// Third-party Imports
|
||||
import type { RankingInfo } from '@tanstack/match-sorter-utils'
|
||||
import { rankItem } from '@tanstack/match-sorter-utils'
|
||||
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedMinMaxValues,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable
|
||||
} from '@tanstack/react-table'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Type Imports
|
||||
import type { InvoiceType } from '@/types/apps/invoiceTypes'
|
||||
import type { Locale } from '@configs/i18n'
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Component Imports
|
||||
import CustomAvatar from '@core/components/mui/Avatar'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
import OptionMenu from '@core/components/option-menu'
|
||||
|
||||
// Util Imports
|
||||
import { getInitials } from '@/utils/getInitials'
|
||||
import { getLocalizedUrl } from '@/utils/i18n'
|
||||
|
||||
// Style Imports
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface FilterFns {
|
||||
fuzzy: FilterFn<unknown>
|
||||
}
|
||||
interface FilterMeta {
|
||||
itemRank: RankingInfo
|
||||
}
|
||||
}
|
||||
|
||||
type InvoiceTypeWithAction = InvoiceType & {
|
||||
action?: string
|
||||
}
|
||||
|
||||
type InvoiceStatusObj = {
|
||||
[key: string]: {
|
||||
icon: string
|
||||
color: ThemeColor
|
||||
}
|
||||
}
|
||||
|
||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value)
|
||||
|
||||
// Store the itemRank info
|
||||
addMeta({
|
||||
itemRank
|
||||
})
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed
|
||||
}
|
||||
|
||||
const DebouncedInput = ({
|
||||
value: initialValue,
|
||||
onChange,
|
||||
debounce = 500,
|
||||
...props
|
||||
}: {
|
||||
value: string | number
|
||||
onChange: (value: string | number) => void
|
||||
debounce?: number
|
||||
} & Omit<TextFieldProps, 'onChange'>) => {
|
||||
// States
|
||||
const [value, setValue] = useState(initialValue)
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue)
|
||||
}, [initialValue])
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
onChange(value)
|
||||
}, debounce)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value])
|
||||
|
||||
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
|
||||
}
|
||||
|
||||
// Vars
|
||||
const invoiceStatusObj: InvoiceStatusObj = {
|
||||
Sent: { color: 'secondary', icon: 'tabler-send-2' },
|
||||
Paid: { color: 'success', icon: 'tabler-check' },
|
||||
Draft: { color: 'primary', icon: 'tabler-mail' },
|
||||
'Partial Payment': { color: 'warning', icon: 'tabler-chart-pie-2' },
|
||||
'Past Due': { color: 'error', icon: 'tabler-alert-circle' },
|
||||
Downloaded: { color: 'info', icon: 'tabler-arrow-down' }
|
||||
}
|
||||
|
||||
// Column Definitions
|
||||
const columnHelper = createColumnHelper<InvoiceTypeWithAction>()
|
||||
|
||||
const InvoiceListTable = ({ invoiceData }: { invoiceData?: InvoiceType[] }) => {
|
||||
// States
|
||||
const [status, setStatus] = useState<InvoiceType['invoiceStatus']>('')
|
||||
const [rowSelection, setRowSelection] = useState({})
|
||||
const [data, setData] = useState(...[invoiceData])
|
||||
const [filteredData, setFilteredData] = useState(data)
|
||||
const [globalFilter, setGlobalFilter] = useState('')
|
||||
|
||||
// Hooks
|
||||
const { lang: locale } = useParams()
|
||||
|
||||
const columns = useMemo<ColumnDef<InvoiceTypeWithAction, any>[]>(
|
||||
() => [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
{...{
|
||||
checked: table.getIsAllRowsSelected(),
|
||||
indeterminate: table.getIsSomeRowsSelected(),
|
||||
onChange: table.getToggleAllRowsSelectedHandler()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
{...{
|
||||
checked: row.getIsSelected(),
|
||||
disabled: !row.getCanSelect(),
|
||||
indeterminate: row.getIsSomeSelected(),
|
||||
onChange: row.getToggleSelectedHandler()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
columnHelper.accessor('id', {
|
||||
header: '#',
|
||||
cell: ({ row }) => (
|
||||
<Typography
|
||||
component={Link}
|
||||
href={getLocalizedUrl(`/apps/invoice/preview/${row.original.id}`, locale as Locale)}
|
||||
color='primary.main'
|
||||
>{`#${row.original.id}`}</Typography>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('invoiceStatus', {
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
<Typography variant='body2' component='span' className='text-inherit'>
|
||||
{row.original.invoiceStatus}
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography variant='body2' component='span' className='text-inherit'>
|
||||
Balance:
|
||||
</Typography>{' '}
|
||||
{row.original.balance}
|
||||
<br />
|
||||
<Typography variant='body2' component='span' className='text-inherit'>
|
||||
Due Date:
|
||||
</Typography>{' '}
|
||||
{row.original.dueDate}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CustomAvatar skin='light' color={invoiceStatusObj[row.original.invoiceStatus].color} size={28}>
|
||||
<i className={classnames('bs-4 is-4', invoiceStatusObj[row.original.invoiceStatus].icon)} />
|
||||
</CustomAvatar>
|
||||
</Tooltip>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('name', {
|
||||
header: 'Client',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center gap-3'>
|
||||
{getAvatar({ avatar: row.original.avatar, name: row.original.name })}
|
||||
<div className='flex flex-col'>
|
||||
<Typography className='font-medium' color='text.primary'>
|
||||
{row.original.name}
|
||||
</Typography>
|
||||
<Typography variant='body2'>{row.original.companyEmail}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor('total', {
|
||||
header: 'Total',
|
||||
cell: ({ row }) => <Typography>{`$${row.original.total}`}</Typography>
|
||||
}),
|
||||
columnHelper.accessor('issuedDate', {
|
||||
header: 'Issued Date',
|
||||
cell: ({ row }) => <Typography>{row.original.issuedDate}</Typography>
|
||||
}),
|
||||
columnHelper.accessor('balance', {
|
||||
header: 'Balance',
|
||||
cell: ({ row }) => {
|
||||
return row.original.balance === 0 ? (
|
||||
<Chip label='Paid' color='success' size='small' variant='tonal' />
|
||||
) : (
|
||||
<Typography color='text.primary'>{row.original.balance}</Typography>
|
||||
)
|
||||
}
|
||||
}),
|
||||
columnHelper.accessor('action', {
|
||||
header: 'Action',
|
||||
cell: ({ row }) => (
|
||||
<div className='flex items-center'>
|
||||
<IconButton onClick={() => setData(data?.filter(invoice => invoice.id !== row.original.id))}>
|
||||
<i className='tabler-trash text-textSecondary' />
|
||||
</IconButton>
|
||||
<IconButton>
|
||||
<Link
|
||||
href={getLocalizedUrl(`/apps/invoice/preview/${row.original.id}`, locale as Locale)}
|
||||
className='flex'
|
||||
>
|
||||
<i className='tabler-eye text-textSecondary' />
|
||||
</Link>
|
||||
</IconButton>
|
||||
<OptionMenu
|
||||
iconButtonProps={{ size: 'medium' }}
|
||||
iconClassName='text-textSecondary'
|
||||
options={[
|
||||
{
|
||||
text: 'Download',
|
||||
icon: 'tabler-download',
|
||||
menuItemProps: { className: 'flex items-center gap-2 text-textSecondary' }
|
||||
},
|
||||
{
|
||||
text: 'Edit',
|
||||
icon: 'tabler-pencil',
|
||||
href: getLocalizedUrl(`/apps/invoice/edit/${row.original.id}`, locale as Locale),
|
||||
linkProps: {
|
||||
className: 'flex items-center is-full plb-2 pli-4 gap-2 text-textSecondary'
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Duplicate',
|
||||
icon: 'tabler-copy',
|
||||
menuItemProps: { className: 'flex items-center gap-2 text-textSecondary' }
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false
|
||||
})
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[data, filteredData]
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredData as InvoiceType[],
|
||||
columns,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter
|
||||
},
|
||||
state: {
|
||||
rowSelection,
|
||||
globalFilter
|
||||
},
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: 10
|
||||
}
|
||||
},
|
||||
enableRowSelection: true, //enable row selection for all rows
|
||||
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
|
||||
globalFilterFn: fuzzyFilter,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
getFacetedMinMaxValues: getFacetedMinMaxValues()
|
||||
})
|
||||
|
||||
const getAvatar = (params: Pick<InvoiceType, 'avatar' | 'name'>) => {
|
||||
const { avatar, name } = params
|
||||
|
||||
if (avatar) {
|
||||
return <CustomAvatar src={avatar} skin='light' size={34} />
|
||||
} else {
|
||||
return (
|
||||
<CustomAvatar skin='light' size={34}>
|
||||
{getInitials(name as string)}
|
||||
</CustomAvatar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const filteredData = data?.filter(invoice => {
|
||||
if (status && invoice.invoiceStatus.toLowerCase().replace(/\s+/g, '-') !== status) return false
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
setFilteredData(filteredData)
|
||||
}, [status, data])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className='flex justify-between flex-col items-start md:items-center md:flex-row gap-4'>
|
||||
<div className='flex items-center justify-between gap-4'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Typography className='hidden sm:block'>Show</Typography>
|
||||
<CustomTextField
|
||||
select
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={e => table.setPageSize(Number(e.target.value))}
|
||||
className='max-sm:is-full sm:is-[70px]'
|
||||
>
|
||||
<MenuItem value='10'>10</MenuItem>
|
||||
<MenuItem value='25'>25</MenuItem>
|
||||
<MenuItem value='50'>50</MenuItem>
|
||||
</CustomTextField>
|
||||
</div>
|
||||
<Button
|
||||
variant='contained'
|
||||
component={Link}
|
||||
startIcon={<i className='tabler-plus' />}
|
||||
href={getLocalizedUrl('apps/invoice/add', locale as Locale)}
|
||||
className='max-sm:is-full'
|
||||
>
|
||||
Create Invoice
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex flex-col sm:flex-row max-sm:is-full items-start sm:items-center gap-4'>
|
||||
<DebouncedInput
|
||||
value={globalFilter ?? ''}
|
||||
onChange={value => setGlobalFilter(String(value))}
|
||||
placeholder='Search Invoice'
|
||||
className='max-sm:is-full sm:is-[250px]'
|
||||
/>
|
||||
<CustomTextField
|
||||
select
|
||||
id='select-status'
|
||||
value={status}
|
||||
onChange={e => setStatus(e.target.value)}
|
||||
className='max-sm:is-full sm:is-[160px]'
|
||||
slotProps={{
|
||||
select: { displayEmpty: true }
|
||||
}}
|
||||
>
|
||||
<MenuItem value=''>Invoice Status</MenuItem>
|
||||
<MenuItem value='downloaded'>Downloaded</MenuItem>
|
||||
<MenuItem value='draft'>Draft</MenuItem>
|
||||
<MenuItem value='paid'>Paid</MenuItem>
|
||||
<MenuItem value='partial-payment'>Partial Payment</MenuItem>
|
||||
<MenuItem value='past-due'>Past Due</MenuItem>
|
||||
<MenuItem value='sent'>Sent</MenuItem>
|
||||
</CustomTextField>
|
||||
</div>
|
||||
</CardContent>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className={tableStyles.table}>
|
||||
<thead>
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<th key={header.id}>
|
||||
{header.isPlaceholder ? null : (
|
||||
<>
|
||||
<div
|
||||
className={classnames({
|
||||
'flex items-center': header.column.getIsSorted(),
|
||||
'cursor-pointer select-none': header.column.getCanSort()
|
||||
})}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{{
|
||||
asc: <i className='tabler-chevron-up text-xl' />,
|
||||
desc: <i className='tabler-chevron-down text-xl' />
|
||||
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
{table.getFilteredRowModel().rows.length === 0 ? (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
|
||||
No data available
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
) : (
|
||||
<tbody>
|
||||
{table
|
||||
.getRowModel()
|
||||
.rows.slice(0, table.getState().pagination.pageSize)
|
||||
.map(row => {
|
||||
return (
|
||||
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
{/* <TablePagination
|
||||
component={() => <TablePaginationComponent table={table} />}
|
||||
count={table.getFilteredRowModel().rows.length}
|
||||
rowsPerPage={table.getState().pagination.pageSize}
|
||||
page={table.getState().pagination.pageIndex}
|
||||
onPageChange={(_, page) => {
|
||||
table.setPageIndex(page)
|
||||
}}
|
||||
onRowsPerPageChange={e => table.setPageSize(Number(e.target.value))}
|
||||
/> */}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default InvoiceListTable
|
||||
@@ -1,225 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Radio from '@mui/material/Radio'
|
||||
import Switch from '@mui/material/Switch'
|
||||
import Button from '@mui/material/Button'
|
||||
import RadioGroup from '@mui/material/RadioGroup'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||
import type { ButtonProps } from '@mui/material/Button'
|
||||
|
||||
// Type Imports
|
||||
import type { ThemeColor } from '@core/types'
|
||||
|
||||
// Component Imports
|
||||
import BillingCard from '@components/dialogs/billing-card'
|
||||
import OpenDialogOnElementClick from '@components/dialogs/OpenDialogOnElementClick'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
type DataType = {
|
||||
cardNumber?: string
|
||||
name?: string
|
||||
expiryDate?: string
|
||||
cardCvv?: string
|
||||
imgSrc?: string
|
||||
imgAlt?: string
|
||||
cardStatus?: string
|
||||
badgeColor?: ThemeColor
|
||||
}
|
||||
|
||||
// Vars
|
||||
const data: DataType[] = [
|
||||
{
|
||||
cardCvv: '587',
|
||||
name: 'Tom McBride',
|
||||
expiryDate: '12/24',
|
||||
imgAlt: 'Mastercard',
|
||||
badgeColor: 'primary',
|
||||
cardStatus: 'Primary',
|
||||
cardNumber: '5577 0000 5577 9865',
|
||||
imgSrc: '/images/logos/mastercard.png'
|
||||
},
|
||||
{
|
||||
cardCvv: '681',
|
||||
name: 'Mildred Wagner',
|
||||
expiryDate: '02/24',
|
||||
imgAlt: 'Visa card',
|
||||
cardNumber: '4532 3616 2070 5678',
|
||||
imgSrc: '/images/logos/visa.png'
|
||||
}
|
||||
]
|
||||
|
||||
const PaymentMethod = () => {
|
||||
// States
|
||||
const [paymentMethod, setPaymentMethod] = useState<'credit' | 'cod'>('credit')
|
||||
const [creditCard, setCreditCard] = useState(0)
|
||||
|
||||
// Hooks
|
||||
const [cardData, setCardData] = useState({
|
||||
cardNumber: '',
|
||||
name: '',
|
||||
expiryDate: '',
|
||||
cardCvv: ''
|
||||
})
|
||||
|
||||
const handleReset = () => {
|
||||
setCardData({
|
||||
cardNumber: '',
|
||||
name: '',
|
||||
expiryDate: '',
|
||||
cardCvv: ''
|
||||
})
|
||||
}
|
||||
|
||||
const buttonProps = (index: number): ButtonProps => ({
|
||||
variant: 'tonal',
|
||||
children: 'Edit',
|
||||
size: 'small',
|
||||
onClick: () => setCreditCard(index)
|
||||
})
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title='Payment Method' />
|
||||
<CardContent>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<RadioGroup
|
||||
row
|
||||
name='payment-method-radio'
|
||||
value={paymentMethod}
|
||||
onChange={e => setPaymentMethod(e.target.value as 'credit' | 'cod')}
|
||||
className='flex gap-4'
|
||||
>
|
||||
<FormControlLabel value='credit' control={<Radio />} label='Credit/Debit/ATM Card' />
|
||||
<FormControlLabel value='cash' control={<Radio />} label='COD/Cheque' />
|
||||
</RadioGroup>
|
||||
</Grid>
|
||||
{paymentMethod === 'credit' ? (
|
||||
<>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
name='number'
|
||||
autoComplete='off'
|
||||
label='Card Number'
|
||||
placeholder='0000 0000 0000 0000'
|
||||
value={cardData.cardNumber}
|
||||
onChange={e => setCardData({ ...cardData, cardNumber: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
name='name'
|
||||
label='Name'
|
||||
autoComplete='off'
|
||||
placeholder='John Doe'
|
||||
value={cardData.name}
|
||||
onChange={e => setCardData({ ...cardData, name: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 3 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
name='expiry'
|
||||
autoComplete='off'
|
||||
label='Expiry Date'
|
||||
placeholder='MM/YY'
|
||||
value={cardData.expiryDate}
|
||||
onChange={e => setCardData({ ...cardData, expiryDate: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, md: 3 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
name='cvv'
|
||||
label='CVV Code'
|
||||
autoComplete='off'
|
||||
placeholder='654'
|
||||
value={cardData.cardCvv}
|
||||
onChange={e => setCardData({ ...cardData, cardCvv: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<FormControlLabel control={<Switch defaultChecked />} label='Save Card for future billing?' />
|
||||
</Grid>
|
||||
</>
|
||||
) : (
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Typography>
|
||||
Cash on delivery is a mode of payment where you make the payment after the goods/services are
|
||||
received.
|
||||
</Typography>
|
||||
<Typography>
|
||||
You can pay cash or make the payment via debit/credit card directly to the delivery person.
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid size={{ xs: 12 }} className='flex gap-4 flex-wrap'>
|
||||
<Button type='submit' variant='contained'>
|
||||
Save Changes
|
||||
</Button>
|
||||
<Button type='reset' variant='tonal' color='secondary' onClick={handleReset}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }} className='flex flex-col gap-6'>
|
||||
<Typography color='text.primary' className='font-medium'>
|
||||
My Cards
|
||||
</Typography>
|
||||
{data.map((item: DataType, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className='flex flex-col rounded bg-actionHover sm:flex-row items-start sm:justify-between max-sm:gap-4 p-6'
|
||||
>
|
||||
<div className='flex flex-col items-start gap-2'>
|
||||
<img src={item.imgSrc} alt={item.imgAlt} />
|
||||
<div className='flex items-center gap-4'>
|
||||
<Typography className='text-textPrimary font-medium'>{item.name}</Typography>
|
||||
{item.cardStatus ? (
|
||||
<Chip color={item.badgeColor} variant='tonal' label={item.cardStatus} size='small' />
|
||||
) : null}
|
||||
</div>
|
||||
<Typography>
|
||||
{item.cardNumber && item.cardNumber.slice(0, -4).replace(/[0-9]/g, '*') + item.cardNumber.slice(-4)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex flex-col sm:items-end gap-4'>
|
||||
<div className='flex gap-4'>
|
||||
<OpenDialogOnElementClick
|
||||
element={Button}
|
||||
elementProps={buttonProps(index)}
|
||||
dialog={BillingCard}
|
||||
dialogProps={{ data: data[creditCard] }}
|
||||
/>
|
||||
<Button variant='tonal' color='error' size='small'>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
<Typography variant='body2'>Card expires at {item.expiryDate}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaymentMethod
|
||||
@@ -1,65 +0,0 @@
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
// Component Imports
|
||||
import CurrentPlan from './CurrentPlan'
|
||||
import Address from './Address'
|
||||
import PaymentMethod from './PaymentMethod'
|
||||
import InvoiceListTable from './InvoiceListTable'
|
||||
|
||||
// Data Imports
|
||||
import { getPricingData, getInvoiceData } from '@/app/server/actions'
|
||||
|
||||
/**
|
||||
* ! If you need data using an API call, uncomment the below API code, update the `process.env.API_URL` variable in the
|
||||
* ! `.env` file found at root of your project and also update the API endpoints like `/pages/pricing` in below example.
|
||||
* ! Also, remove the above server action import and the action itself from the `src/app/server/actions.ts` file to clean up unused code
|
||||
* ! because we've used the server action for getting our static data.
|
||||
*/
|
||||
|
||||
/* const getPricingData = async () => {
|
||||
// Vars
|
||||
const res = await fetch(`${process.env.API_URL}/pages/pricing`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
|
||||
return res.json()
|
||||
} */
|
||||
|
||||
/* const getInvoiceData = async () => {
|
||||
// Vars
|
||||
const res = await fetch(`${process.env.API_URL}/apps/invoice`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch invoice data')
|
||||
}
|
||||
|
||||
return res.json()
|
||||
} */
|
||||
|
||||
const BillingPlans = async () => {
|
||||
// Vars
|
||||
const data = await getPricingData()
|
||||
const invoiceData = await getInvoiceData()
|
||||
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CurrentPlan data={data} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<PaymentMethod />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Address />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<InvoiceListTable invoiceData={invoiceData} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default BillingPlans
|
||||
@@ -1,156 +0,0 @@
|
||||
// Next Imports
|
||||
import Link from 'next/link'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Switch from '@mui/material/Switch'
|
||||
|
||||
// Component Imports
|
||||
import CustomIconButton from '@core/components/mui/IconButton'
|
||||
|
||||
type ConnectedAccountsType = {
|
||||
title: string
|
||||
logo: string
|
||||
checked: boolean
|
||||
subtitle: string
|
||||
}
|
||||
|
||||
type SocialAccountsType = {
|
||||
title: string
|
||||
logo: string
|
||||
username?: string
|
||||
isConnected: boolean
|
||||
href?: string
|
||||
}
|
||||
|
||||
// Vars
|
||||
const connectedAccountsArr: ConnectedAccountsType[] = [
|
||||
{
|
||||
checked: true,
|
||||
title: 'Google',
|
||||
logo: '/images/logos/google.png',
|
||||
subtitle: 'Calendar and Contacts'
|
||||
},
|
||||
{
|
||||
checked: false,
|
||||
title: 'Slack',
|
||||
logo: '/images/logos/slack.png',
|
||||
subtitle: 'Communications'
|
||||
},
|
||||
{
|
||||
checked: true,
|
||||
title: 'Github',
|
||||
logo: '/images/logos/github.png',
|
||||
subtitle: 'Manage your Git repositories'
|
||||
},
|
||||
{
|
||||
checked: true,
|
||||
title: 'Mailchimp',
|
||||
subtitle: 'Email marketing service',
|
||||
logo: '/images/logos/mailchimp.png'
|
||||
},
|
||||
{
|
||||
title: 'Asana',
|
||||
checked: false,
|
||||
subtitle: 'Task Communication',
|
||||
logo: '/images/logos/asana.png'
|
||||
}
|
||||
]
|
||||
|
||||
const socialAccountsArr: SocialAccountsType[] = [
|
||||
{
|
||||
title: 'Facebook',
|
||||
isConnected: false,
|
||||
logo: '/images/logos/facebook.png'
|
||||
},
|
||||
{
|
||||
title: 'Twitter',
|
||||
isConnected: true,
|
||||
username: '@Pixinvent',
|
||||
logo: '/images/logos/twitter.png',
|
||||
href: 'https://twitter.com/pixinvents'
|
||||
},
|
||||
{
|
||||
title: 'Linkedin',
|
||||
isConnected: true,
|
||||
username: '@Pixinvent',
|
||||
logo: '/images/logos/linkedin.png',
|
||||
href: 'https://in.linkedin.com/company/pixinvent'
|
||||
},
|
||||
{
|
||||
title: 'Dribbble',
|
||||
isConnected: false,
|
||||
logo: '/images/logos/dribbble.png'
|
||||
},
|
||||
{
|
||||
title: 'Behance',
|
||||
isConnected: false,
|
||||
logo: '/images/logos/behance.png'
|
||||
}
|
||||
]
|
||||
|
||||
const Connections = () => {
|
||||
return (
|
||||
<Card>
|
||||
<Grid container>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<CardHeader
|
||||
title='Connected Accounts'
|
||||
subheader='Display content from your connected accounts on your site'
|
||||
/>
|
||||
<CardContent className='flex flex-col gap-4'>
|
||||
{connectedAccountsArr.map((item, index) => (
|
||||
<div key={index} className='flex items-center justify-between gap-4'>
|
||||
<div className='flex flex-grow items-center gap-4'>
|
||||
<img height={32} width={32} src={item.logo} alt={item.title} />
|
||||
<div className='flex-grow'>
|
||||
<Typography className='text-textPrimary font-medium'>{item.title}</Typography>
|
||||
<Typography variant='body2'>{item.subtitle}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Switch defaultChecked={item.checked} />
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<CardHeader title='Social Accounts' subheader='Display content from social accounts on your site' />
|
||||
<CardContent className='flex flex-col gap-4'>
|
||||
{socialAccountsArr.map((item, index) => (
|
||||
<div key={index} className='flex items-center justify-between gap-4'>
|
||||
<div className='flex flex-grow items-center gap-4'>
|
||||
<img height={32} width={32} src={item.logo} alt={item.title} />
|
||||
<div className='flex-grow'>
|
||||
<Typography className='text-textPrimary font-medium'>{item.title}</Typography>
|
||||
{item.isConnected ? (
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='primary.main'
|
||||
component={Link}
|
||||
href={item.href || '/'}
|
||||
target='_blank'
|
||||
>
|
||||
{item.username}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant='body2'>Not Connected</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CustomIconButton variant='tonal' color={item.isConnected ? 'error' : 'secondary'}>
|
||||
<i className={item.isConnected ? 'tabler-trash' : 'tabler-link'} />
|
||||
</CustomIconButton>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default Connections
|
||||
@@ -1,56 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import { useState } from 'react'
|
||||
import type { SyntheticEvent, ReactElement } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Tab from '@mui/material/Tab'
|
||||
import TabContext from '@mui/lab/TabContext'
|
||||
import TabPanel from '@mui/lab/TabPanel'
|
||||
|
||||
// Component Imports
|
||||
import CustomTabList from '@core/components/mui/TabList'
|
||||
|
||||
const AccountSettings = ({ tabContentList }: { tabContentList: { [key: string]: ReactElement } }) => {
|
||||
// States
|
||||
const [activeTab, setActiveTab] = useState('account')
|
||||
|
||||
const handleChange = (event: SyntheticEvent, value: string) => {
|
||||
setActiveTab(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<TabContext value={activeTab}>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CustomTabList onChange={handleChange} variant='scrollable' pill='true'>
|
||||
<Tab label='Account' icon={<i className='tabler-users' />} iconPosition='start' value='account' />
|
||||
<Tab label='Security' icon={<i className='tabler-lock' />} iconPosition='start' value='security' />
|
||||
<Tab
|
||||
label='Billing & Plans'
|
||||
icon={<i className='tabler-bookmark' />}
|
||||
iconPosition='start'
|
||||
value='billing-plans'
|
||||
/>
|
||||
<Tab
|
||||
label='Notifications'
|
||||
icon={<i className='tabler-bell' />}
|
||||
iconPosition='start'
|
||||
value='notifications'
|
||||
/>
|
||||
<Tab label='Connections' icon={<i className='tabler-link' />} iconPosition='start' value='connections' />
|
||||
</CustomTabList>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TabPanel value={activeTab} className='p-0'>
|
||||
{tabContentList[activeTab]}
|
||||
</TabPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabContext>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountSettings
|
||||
@@ -1,123 +0,0 @@
|
||||
'use client'
|
||||
|
||||
// MUI Imports
|
||||
import Card from '@mui/material/Card'
|
||||
import CardHeader from '@mui/material/CardHeader'
|
||||
import CardContent from '@mui/material/CardContent'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
import Button from '@mui/material/Button'
|
||||
|
||||
// Component Imports
|
||||
import Link from '@components/Link'
|
||||
import Form from '@components/Form'
|
||||
import CustomTextField from '@core/components/mui/TextField'
|
||||
|
||||
// Style Imports
|
||||
import tableStyles from '@core/styles/table.module.css'
|
||||
|
||||
type TableDataType = {
|
||||
type: string
|
||||
app: boolean
|
||||
email: boolean
|
||||
browser: boolean
|
||||
}
|
||||
|
||||
// Vars
|
||||
const tableData: TableDataType[] = [
|
||||
{
|
||||
app: true,
|
||||
email: true,
|
||||
browser: true,
|
||||
type: 'New for you'
|
||||
},
|
||||
{
|
||||
app: true,
|
||||
email: true,
|
||||
browser: true,
|
||||
type: 'Account activity'
|
||||
},
|
||||
{
|
||||
app: false,
|
||||
email: true,
|
||||
browser: true,
|
||||
type: 'A new browser used to sign in'
|
||||
},
|
||||
{
|
||||
app: false,
|
||||
email: true,
|
||||
browser: false,
|
||||
type: 'A new device is linked'
|
||||
}
|
||||
]
|
||||
|
||||
const Notifications = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title='Recent Devices'
|
||||
subheader={
|
||||
<>
|
||||
We need permission from your browser to show notifications.
|
||||
<Link className='text-primary'> Request Permission</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Form>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className={tableStyles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Email</th>
|
||||
<th>Browser</th>
|
||||
<th>App</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className='border-be'>
|
||||
{tableData.map((data, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<Typography color='text.primary'>{data.type}</Typography>
|
||||
</td>
|
||||
<td>
|
||||
<Checkbox defaultChecked={data.email} />
|
||||
</td>
|
||||
<td>
|
||||
<Checkbox defaultChecked={data.browser} />
|
||||
</td>
|
||||
<td>
|
||||
<Checkbox defaultChecked={data.app} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<CardContent>
|
||||
<Typography className='mbe-6 font-medium'>When should we send you notifications?</Typography>
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<CustomTextField select fullWidth defaultValue='online'>
|
||||
<MenuItem value='online'>Only when I'm online</MenuItem>
|
||||
<MenuItem value='anytime'>Anytime</MenuItem>
|
||||
</CustomTextField>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }} className='flex gap-4 flex-wrap'>
|
||||
<Button variant='contained' type='submit'>
|
||||
Save Changes
|
||||
</Button>
|
||||
<Button variant='tonal' color='secondary' type='reset'>
|
||||
Discard
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Form>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default Notifications
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user