fix: clean folder

This commit is contained in:
ferdiansyah783
2025-08-07 03:53:40 +07:00
parent 5f24ec7899
commit a72a64215a
208 changed files with 174 additions and 29968 deletions
@@ -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&apos;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&apos;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
-359
View File
@@ -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
-193
View File
@@ -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
-137
View File
@@ -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
-72
View File
@@ -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
-227
View File
@@ -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
-230
View File
@@ -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
-241
View File
@@ -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
-293
View File
@@ -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
-183
View File
@@ -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
-172
View File
@@ -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
-121
View File
@@ -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 users 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
-20
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
-263
View File
@@ -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
-95
View File
@@ -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
-152
View File
@@ -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
-226
View File
@@ -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
-230
View File
@@ -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
-477
View File
@@ -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
-195
View File
@@ -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
-125
View File
@@ -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
-58
View File
@@ -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;
}
-99
View File
@@ -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
-372
View File
@@ -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
-114
View File
@@ -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
-342
View File
@@ -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
-24
View File
@@ -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
-31
View File
@@ -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
-31
View File
@@ -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
-85
View File
@@ -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
-268
View File
@@ -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
-215
View File
@@ -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
-118
View File
@@ -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
-122
View File
@@ -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
-202
View File
@@ -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
-42
View File
@@ -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
-144
View File
@@ -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&#39;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