feat(envelope-generator-react-ui): init

This commit is contained in:
2025-07-21 15:13:16 +02:00
parent 286e17a900
commit cce240125d
242 changed files with 18884 additions and 4 deletions

View File

@@ -0,0 +1,210 @@
import {
_id,
_price,
_times,
_company,
_boolean,
_fullName,
_taskNames,
_postTitles,
_description,
_productNames,
} from './_mock';
// ----------------------------------------------------------------------
export const _myAccount = {
displayName: 'Jaydon Frankie',
email: 'demo@minimals.cc',
photoURL: '/assets/images/avatar/avatar-25.webp',
};
// ----------------------------------------------------------------------
export const _users = [...Array(24)].map((_, index) => ({
id: _id(index),
name: _fullName(index),
company: _company(index),
isVerified: _boolean(index),
avatarUrl: `/assets/images/avatar/avatar-${index + 1}.webp`,
status: index % 4 ? 'active' : 'banned',
role:
[
'Leader',
'Hr Manager',
'UI Designer',
'UX Designer',
'UI/UX Designer',
'Project Manager',
'Backend Developer',
'Full Stack Designer',
'Front End Developer',
'Full Stack Developer',
][index] || 'UI Designer',
}));
// ----------------------------------------------------------------------
export const _posts = [...Array(23)].map((_, index) => ({
id: _id(index),
title: _postTitles(index),
description: _description(index),
coverUrl: `/assets/images/cover/cover-${index + 1}.webp`,
totalViews: 8829,
totalComments: 7977,
totalShares: 8556,
totalFavorites: 8870,
postedAt: _times(index),
author: {
name: _fullName(index),
avatarUrl: `/assets/images/avatar/avatar-${index + 1}.webp`,
},
}));
// ----------------------------------------------------------------------
const COLORS = [
'#00AB55',
'#000000',
'#FFFFFF',
'#FFC0CB',
'#FF4842',
'#1890FF',
'#94D82D',
'#FFC107',
];
export const _products = [...Array(24)].map((_, index) => {
const setIndex = index + 1;
return {
id: _id(index),
price: _price(index),
name: _productNames(index),
priceSale: setIndex % 3 ? null : _price(index),
coverUrl: `/assets/images/product/product-${setIndex}.webp`,
colors:
(setIndex === 1 && COLORS.slice(0, 2)) ||
(setIndex === 2 && COLORS.slice(1, 3)) ||
(setIndex === 3 && COLORS.slice(2, 4)) ||
(setIndex === 4 && COLORS.slice(3, 6)) ||
(setIndex === 23 && COLORS.slice(4, 6)) ||
(setIndex === 24 && COLORS.slice(5, 6)) ||
COLORS,
status:
([1, 3, 5].includes(setIndex) && 'sale') || ([4, 8, 12].includes(setIndex) && 'new') || '',
};
});
// ----------------------------------------------------------------------
export const _langs = [
{
value: 'en',
label: 'English',
icon: '/assets/icons/flags/ic-flag-en.svg',
},
{
value: 'de',
label: 'German',
icon: '/assets/icons/flags/ic-flag-de.svg',
},
{
value: 'fr',
label: 'French',
icon: '/assets/icons/flags/ic-flag-fr.svg',
},
];
// ----------------------------------------------------------------------
export const _timeline = [...Array(5)].map((_, index) => ({
id: _id(index),
title: [
'1983, orders, $4220',
'12 Invoices have been paid',
'Order #37745 from September',
'New order placed #XF-2356',
'New order placed #XF-2346',
][index],
type: `order${index + 1}`,
time: _times(index),
}));
export const _traffic = [
{
value: 'facebook',
label: 'Facebook',
total: 19500,
},
{
value: 'google',
label: 'Google',
total: 91200,
},
{
value: 'linkedin',
label: 'Linkedin',
total: 69800,
},
{
value: 'twitter',
label: 'Twitter',
total: 84900,
},
];
export const _tasks = Array.from({ length: 5 }, (_, index) => ({
id: _id(index),
name: _taskNames(index),
}));
// ----------------------------------------------------------------------
export const _notifications = [
{
id: _id(1),
title: 'Your order is placed',
description: 'waiting for shipping',
avatarUrl: null,
type: 'order-placed',
postedAt: _times(1),
isUnRead: true,
},
{
id: _id(2),
title: _fullName(2),
description: 'answered to your comment on the Minimal',
avatarUrl: '/assets/images/avatar/avatar-2.webp',
type: 'friend-interactive',
postedAt: _times(2),
isUnRead: true,
},
{
id: _id(3),
title: 'You have new message',
description: '5 unread messages',
avatarUrl: null,
type: 'chat-message',
postedAt: _times(3),
isUnRead: false,
},
{
id: _id(4),
title: 'You have new mail',
description: 'sent from Guido Padberg',
avatarUrl: null,
type: 'mail',
postedAt: _times(4),
isUnRead: false,
},
{
id: _id(5),
title: 'Delivery processing',
description: 'Your order is being shipped',
avatarUrl: null,
type: 'order-shipped',
postedAt: _times(5),
isUnRead: false,
},
];

View File

@@ -0,0 +1,232 @@
export const _id = (index: number) => `e99f09a7-dd88-49d5-b1c8-1daf80c2d7b${index}`;
export const _times = (index: number) =>
// 'MM/DD/YYYY'
[
'11/08/2023',
'04/09/2024',
'09/12/2023',
'01/01/2024',
'04/23/2024',
'02/29/2024',
'05/14/2024',
'01/13/2024',
'06/22/2024',
'10/05/2023',
'07/11/2024',
'05/22/2024',
'03/29/2024',
'08/29/2023',
'11/19/2023',
'10/24/2023',
'12/02/2023',
'02/13/2024',
'09/19/2023',
'04/17/2024',
'12/18/2023',
'06/27/2024',
'10/19/2023',
'08/09/2024',
][index];
export const _fullName = (index: number) =>
[
'Billy Stoltenberg',
'Eloise Ebert',
'Teresa Luettgen',
'Salvador Mayert',
'Dr. Guadalupe Rath',
'Kelvin Pouros',
'Thelma Langworth',
'Kristen Wunsch',
'Steve Welch',
'Brian Jacobs',
'Lillie Schultz',
'Mr. Conrad Spinka',
'Charlene Krajcik',
'Kerry Kuhlman',
'Betty Hammes',
'Tony Paucek PhD',
'Sherri Davis',
'Angel Rolfson-Kulas',
'Dr. Lee Doyle-Grant',
'Cheryl Romaguera',
'Billy Braun',
'Adam Trantow',
'Brandon Von',
'Willis Ankunding',
][index];
export const _price = (index: number) =>
[
35.17, 57.22, 64.78, 50.79, 9.57, 61.46, 96.73, 63.04, 33.18, 36.3, 54.42, 20.52, 62.82, 19.96,
25.93, 70.39, 23.11, 67.23, 14.31, 31.5, 26.72, 44.8, 37.87, 75.53,
][index];
export const _company = (index: number) =>
[
'Medhurst, Moore and Franey',
'Hahn, Homenick and Lind',
'Larkin LLC',
'Stamm, Larson and Mertz',
'Spencer, Raynor and Langosh',
'Lehner - Feeney',
'Leuschke, Harris and Kuhlman',
'Gutmann - Kassulke',
'Turcotte - Runolfsson',
'Howe - Anderson',
'Sipes - Yost',
'Johns - Aufderhar',
'Schmidt LLC',
'Smitham - Gerlach',
'Waelchi - VonRueden',
'Padberg - Macejkovic',
'Lemke - Ferry',
'Koch and Sons',
'Klein - Rolfson',
'Weimann LLC',
'White, Cassin and Goldner',
'Mohr, Langworth and Hills',
'Mitchell, Volkman and Prosacco',
'Streich Group',
][index];
export const _boolean = (index: number) =>
[
true,
false,
true,
false,
true,
true,
true,
false,
false,
true,
false,
true,
true,
false,
true,
false,
false,
true,
false,
false,
false,
true,
true,
false,
][index];
export const _postTitles = (index: number) =>
[
'Whiteboard Templates By Industry Leaders',
'Tesla Cybertruck-inspired camper trailer for Tesla fans who cant just wait for the truck!',
'Designify Agency Landing Page Design',
'✨What is Done is Done ✨',
'Fresh Prince',
'Six Socks Studio',
'vincenzo de cotiis crossing over showcases a research on contamination',
'Simple, Great Looking Animations in Your Project | Video Tutorial',
'40 Free Serif Fonts for Digital Designers',
'Examining the Evolution of the Typical Web Design Client',
'Katie Griffin loves making that homey art',
'The American Dream retold through mid-century railroad graphics',
'Illustration System Design',
'CarZio-Delivery Driver App SignIn/SignUp',
'How to create a client-serverless Jamstack app using Netlify, Gatsby and Fauna',
'Tylko Organise effortlessly -3D & Motion Design',
'RAYO ?? A expanded visual arts festival identity',
'Anthony Burrill and Wired mags Andrew Diprose discuss how they made Januarys Change Everything cover',
'Inside the Mind of Samuel Day',
'Portfolio Review: Is This Portfolio Too Creative?',
'Akkers van Margraten',
'Gradient Ticket icon',
'Heres a Dyson motorcycle concept that doesnt suck!',
'How to Animate a SVG with border-image',
][index];
export const _description = (index: number) =>
[
'The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J',
'New range of formal shirts are designed keeping you in mind. With fits and styling that will make you stand apart',
'Andy shoes are designed to keeping in mind durability as well as trends, the most stylish range of shoes & sandals',
'The Football Is Good For Training And Recreational Purposes',
'New ABC 13 9370, 13.3, 5th Gen CoreA5-8250U, 8GB RAM, 256GB SSD, power UHD Graphics, OS 10 Home, OS Office A & J 2016',
'Andy shoes are designed to keeping in mind durability as well as trends, the most stylish range of shoes & sandals',
'Carbonite web goalkeeper gloves are ergonomically designed to give easy fit',
'The Apollotech B340 is an affordable wireless mouse with reliable connectivity, 12 months battery life and modern design',
'The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J',
'The automobile layout consists of a front-engine design, with transaxle-type transmissions mounted at the rear of the engine and four wheel drive',
'The automobile layout consists of a front-engine design, with transaxle-type transmissions mounted at the rear of the engine and four wheel drive',
'The Apollotech B340 is an affordable wireless mouse with reliable connectivity, 12 months battery life and modern design',
'New range of formal shirts are designed keeping you in mind. With fits and styling that will make you stand apart',
"Boston's most advanced compression wear technology increases muscle oxygenation, stabilizes active muscles",
'New range of formal shirts are designed keeping you in mind. With fits and styling that will make you stand apart',
'Andy shoes are designed to keeping in mind durability as well as trends, the most stylish range of shoes & sandals',
'Andy shoes are designed to keeping in mind durability as well as trends, the most stylish range of shoes & sandals',
'The beautiful range of Apple Naturalé that has an exciting mix of natural ingredients. With the Goodness of 100% Natural Ingredients',
"Boston's most advanced compression wear technology increases muscle oxygenation, stabilizes active muscles",
'New ABC 13 9370, 13.3, 5th Gen CoreA5-8250U, 8GB RAM, 256GB SSD, power UHD Graphics, OS 10 Home, OS Office A & J 2016',
'The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J',
'Ergonomic executive chair upholstered in bonded black leather and PVC padded seat and back for all-day comfort and support',
'The Football Is Good For Training And Recreational Purposes',
'The automobile layout consists of a front-engine design, with transaxle-type transmissions mounted at the rear of the engine and four wheel drive',
][index];
export const _taskNames = (index: number) =>
[
`Prepare Monthly Financial Report`,
`Design New Marketing Campaign`,
`Analyze Customer Feedback`,
`Update Website Content`,
`Conduct Market Research`,
`Develop Software Application`,
`Organize Team Meeting`,
`Create Social Media Posts`,
`Review Project Plan`,
`Implement Security Protocols`,
`Write Technical Documentation`,
`Test New Product Features`,
`Manage Client Inquiries`,
`Train New Employees`,
`Coordinate Logistics`,
`Monitor Network Performance`,
`Develop Training Materials`,
`Draft Press Release`,
`Prepare Budget Proposal`,
`Evaluate Vendor Proposals`,
`Perform Data Analysis`,
`Conduct Quality Assurance`,
`Plan Event Logistics`,
`Optimize SEO Strategies`,
][index];
export const _productNames = (index: number) =>
[
'Nike Air Force 1 NDESTRUKT',
'Nike Space Hippie 04',
'Nike Air Zoom Pegasus 37 A.I.R. Chaz Bear',
'Nike Blazer Low 77 Vintage',
'Nike ZoomX SuperRep Surge',
'Zoom Freak 2',
'Nike Air Max Zephyr',
'Jordan Delta',
'Air Jordan XXXV PF',
'Nike Waffle Racer Crater',
'Kyrie 7 EP Sisterhood',
'Nike Air Zoom BB NXT',
'Nike Air Force 1 07 LX',
'Nike Air Force 1 Shadow SE',
'Nike Air Zoom Tempo NEXT%',
'Nike DBreak-Type',
'Nike Air Max Up',
'Nike Air Max 270 React ENG',
'NikeCourt Royale',
'Nike Air Zoom Pegasus 37 Premium',
'Nike Air Zoom SuperRep',
'NikeCourt Royale',
'Nike React Art3mis',
'Nike React Infinity Run Flyknit A.I.R. Chaz Bear',
][index];

View File

@@ -0,0 +1,2 @@
export * from './_mock';
export * from './_data';

View File

@@ -0,0 +1,59 @@
import 'src/global.css';
import { useEffect } from 'react';
import Fab from '@mui/material/Fab';
import { usePathname } from 'src/routes/hooks';
import { ThemeProvider } from 'src/theme/theme-provider';
import { Iconify } from 'src/components/iconify';
// ----------------------------------------------------------------------
type AppProps = {
children: React.ReactNode;
};
export default function App({ children }: AppProps) {
useScrollToTop();
const githubButton = () => (
<Fab
size="medium"
aria-label="Github"
href="https://github.com/minimal-ui-kit/material-kit-react"
sx={{
zIndex: 9,
right: 20,
bottom: 20,
width: 48,
height: 48,
position: 'fixed',
bgcolor: 'grey.800',
}}
>
<Iconify width={24} icon="socials:github" sx={{ '--color': 'white' }} />
</Fab>
);
return (
<ThemeProvider>
{children}
{githubButton()}
</ThemeProvider>
);
}
// ----------------------------------------------------------------------
function useScrollToTop() {
const pathname = usePathname();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}

View File

@@ -0,0 +1,48 @@
import { lazy, Suspense } from 'react';
import { useIsClient } from 'minimal-shared/hooks';
import { mergeClasses } from 'minimal-shared/utils';
import { styled } from '@mui/material/styles';
import { chartClasses } from './classes';
import { ChartLoading } from './components';
import type { ChartProps } from './types';
// ----------------------------------------------------------------------
const LazyChart = lazy(() =>
import('react-apexcharts').then((module) => ({ default: module.default }))
);
export function Chart({ type, series, options, slotProps, className, sx, ...other }: ChartProps) {
const isClient = useIsClient();
const renderFallback = () => <ChartLoading type={type} sx={slotProps?.loading} />;
return (
<ChartRoot
dir="ltr"
className={mergeClasses([chartClasses.root, className])}
sx={sx}
{...other}
>
{isClient ? (
<Suspense fallback={renderFallback()}>
<LazyChart type={type} series={series} options={options} width="100%" height="100%" />
</Suspense>
) : (
renderFallback()
)}
</ChartRoot>
);
}
// ----------------------------------------------------------------------
const ChartRoot = styled('div')(({ theme }) => ({
width: '100%',
flexShrink: 0,
position: 'relative',
borderRadius: theme.shape.borderRadius * 1.5,
}));

View File

@@ -0,0 +1,19 @@
import { createClasses } from 'src/theme/create-classes';
// ----------------------------------------------------------------------
export const chartClasses = {
root: createClasses('chart__root'),
loading: createClasses('chart__loading'),
legends: {
root: createClasses('chart__legends__root'),
item: {
wrap: createClasses('chart__legends__item__wrap'),
root: createClasses('chart__legends__item__root'),
dot: createClasses('chart__legends__item__dot'),
icon: createClasses('chart__legends__item__icon'),
label: createClasses('chart__legends__item__label'),
value: createClasses('chart__legends__item__value'),
},
},
};

View File

@@ -0,0 +1,128 @@
import { mergeClasses } from 'minimal-shared/utils';
import { styled } from '@mui/material/styles';
import { chartClasses } from '../classes';
// ----------------------------------------------------------------------
export type ChartLegendsProps = React.ComponentProps<typeof ListRoot> & {
labels?: string[];
colors?: string[];
values?: string[];
sublabels?: string[];
icons?: React.ReactNode[];
slotProps?: {
wrapper?: React.ComponentProps<typeof ItemWrap>;
root?: React.ComponentProps<typeof ItemRoot>;
dot?: React.ComponentProps<typeof ItemDot>;
icon?: React.ComponentProps<typeof ItemIcon>;
value?: React.ComponentProps<typeof ItemValue>;
label?: React.ComponentProps<typeof ItemLabel>;
};
};
export function ChartLegends({
sx,
className,
slotProps,
icons = [],
values = [],
labels = [],
colors = [],
sublabels = [],
...other
}: ChartLegendsProps) {
return (
<ListRoot className={mergeClasses([chartClasses.legends.root, className])} sx={sx} {...other}>
{labels.map((series, index) => (
<ItemWrap
key={series}
className={chartClasses.legends.item.wrap}
sx={[
{
'--icon-color': colors[index],
...slotProps?.wrapper,
},
...(Array.isArray(slotProps?.wrapper?.sx)
? (slotProps?.wrapper?.sx ?? [])
: [slotProps?.wrapper?.sx]),
]}
>
<ItemRoot className={chartClasses.legends.item.root} {...slotProps?.root}>
{icons.length ? (
<ItemIcon className={chartClasses.legends.item.icon} {...slotProps?.icon}>
{icons[index]}
</ItemIcon>
) : (
<ItemDot className={chartClasses.legends.item.dot} {...slotProps?.dot} />
)}
<ItemLabel className={chartClasses.legends.item.label} {...slotProps?.label}>
{series}
{!!sublabels.length && <> {` (${sublabels[index]})`}</>}
</ItemLabel>
</ItemRoot>
{values && (
<ItemValue className={chartClasses.legends.item.value} {...slotProps?.value}>
{values[index]}
</ItemValue>
)}
</ItemWrap>
))}
</ListRoot>
);
}
// ----------------------------------------------------------------------
const ListRoot = styled('ul')(({ theme }) => ({
display: 'flex',
flexWrap: 'wrap',
gap: theme.spacing(2),
}));
const ItemWrap = styled('li')(() => ({
display: 'inline-flex',
flexDirection: 'column',
}));
const ItemRoot = styled('div')(({ theme }) => ({
gap: 6,
alignItems: 'center',
display: 'inline-flex',
justifyContent: 'flex-start',
fontSize: theme.typography.pxToRem(13),
fontWeight: theme.typography.fontWeightMedium,
}));
const ItemIcon = styled('span')({
display: 'inline-flex',
color: 'var(--icon-color)',
/**
* As ':first-child' for ssr
* https://github.com/emotion-js/emotion/issues/1105#issuecomment-1126025608
*/
'& > :first-of-type:not(style):not(:first-of-type ~ *), & > style + *': { width: 20, height: 20 },
});
const ItemDot = styled('span')({
width: 12,
height: 12,
flexShrink: 0,
display: 'flex',
borderRadius: '50%',
position: 'relative',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--icon-color)',
backgroundColor: 'currentColor',
});
const ItemLabel = styled('span')({ flexShrink: 0 });
const ItemValue = styled('span')(({ theme }) => ({
...theme.typography.h6,
marginTop: theme.spacing(1),
}));

View File

@@ -0,0 +1,51 @@
import type { BoxProps } from '@mui/material/Box';
import { mergeClasses } from 'minimal-shared/utils';
import Box from '@mui/material/Box';
import Skeleton from '@mui/material/Skeleton';
import { chartClasses } from '../classes';
import type { ChartProps } from '../types';
// ----------------------------------------------------------------------
export type ChartLoadingProps = BoxProps & Pick<ChartProps, 'type'>;
export function ChartLoading({ sx, className, type, ...other }: ChartLoadingProps) {
const circularTypes: ChartProps['type'][] = ['donut', 'radialBar', 'pie', 'polarArea'];
return (
<Box
className={mergeClasses([chartClasses.loading, className])}
sx={[
() => ({
top: 0,
left: 0,
width: 1,
zIndex: 9,
height: 1,
p: 'inherit',
overflow: 'hidden',
alignItems: 'center',
position: 'absolute',
borderRadius: 'inherit',
justifyContent: 'center',
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
<Skeleton
variant="circular"
sx={{
width: 1,
height: 1,
borderRadius: 'inherit',
...(circularTypes.includes(type) && { borderRadius: '50%' }),
}}
/>
</Box>
);
}

View File

@@ -0,0 +1,3 @@
export * from './chart-legends';
export * from './chart-loading';

View File

@@ -0,0 +1,7 @@
export * from './chart';
export * from './use-chart';
export * from './components';
export type * from './types';

View File

@@ -0,0 +1,56 @@
.apexcharts-canvas {
/**
* Tooltip
*/
.apexcharts-tooltip {
min-width: 80px;
border-radius: 10px;
backdrop-filter: blur(6px);
color: var(--palette-text-primary);
box-shadow: var(--customShadows-dropdown);
background-color: rgba(var(--palette-background-defaultChannel) / 0.9);
}
.apexcharts-xaxistooltip {
border-radius: 10px;
border-color: transparent;
backdrop-filter: blur(6px);
color: var(--palette-text-primary);
box-shadow: var(--customShadows-dropdown);
background-color: rgba(var(--palette-background-defaultChannel) / 0.9);
&::before {
border-bottom-color: rgba(var(--palette-grey-500Channel) / 0.16);
}
&::after {
border-bottom-color: rgba(var(--palette-background-defaultChannel) / 0.9);
}
}
.apexcharts-tooltip-title {
font-weight: 700;
text-align: center;
color: var(--palette-text-secondary);
background-color: var(--palette-background-neutral);
}
/**
* Tooltip: group
*/
.apexcharts-tooltip-series-group {
padding: 4px 12px;
}
.apexcharts-tooltip-marker {
margin-right: 8px;
}
/**
* Legend
*/
.apexcharts-legend {
padding: 0;
}
.apexcharts-legend-marker {
margin-right: 6px;
}
.apexcharts-legend-text {
margin-left: 0;
padding-left: 0;
line-height: 18px;
}
}

View File

@@ -0,0 +1,14 @@
import type { Theme, SxProps } from '@mui/material/styles';
import type { Props as ApexProps } from 'react-apexcharts';
// ----------------------------------------------------------------------
export type ChartOptions = ApexProps['options'];
export type ChartProps = React.ComponentProps<'div'> &
Pick<ApexProps, 'type' | 'series' | 'options'> & {
sx?: SxProps<Theme>;
slotProps?: {
loading?: SxProps<Theme>;
};
};

View File

@@ -0,0 +1,227 @@
import type { Theme } from '@mui/material/styles';
import { merge } from 'es-toolkit';
import { varAlpha } from 'minimal-shared/utils';
import { useTheme } from '@mui/material/styles';
import type { ChartOptions } from './types';
// ----------------------------------------------------------------------
export function useChart(updatedOptions?: ChartOptions): ChartOptions {
const theme = useTheme();
const baseOptions = baseChartOptions(theme) ?? {};
return merge(baseOptions, updatedOptions ?? {});
}
// ----------------------------------------------------------------------
const baseChartOptions = (theme: Theme): ChartOptions => {
const LABEL_TOTAL = {
show: true,
label: 'Total',
color: theme.vars.palette.text.secondary,
fontSize: theme.typography.subtitle2.fontSize as string,
fontWeight: theme.typography.subtitle2.fontWeight,
};
const LABEL_VALUE = {
offsetY: 8,
color: theme.vars.palette.text.primary,
fontSize: theme.typography.h4.fontSize as string,
fontWeight: theme.typography.h4.fontWeight,
};
return {
/** **************************************
* Chart
* https://apexcharts.com/docs/options/chart/animations/
*************************************** */
chart: {
toolbar: { show: false },
zoom: { enabled: false },
parentHeightOffset: 0,
fontFamily: theme.typography.fontFamily,
foreColor: theme.vars.palette.text.disabled,
animations: {
enabled: true,
speed: 360,
animateGradually: { enabled: true, delay: 120 },
dynamicAnimation: { enabled: true, speed: 360 },
},
},
/** **************************************
* Colors
* https://apexcharts.com/docs/options/colors/
*************************************** */
colors: [
theme.palette.primary.main,
theme.palette.warning.main,
theme.palette.info.main,
theme.palette.error.main,
theme.palette.success.main,
theme.palette.warning.dark,
theme.palette.success.darker,
theme.palette.info.dark,
theme.palette.info.darker,
],
/** **************************************
* States
* https://apexcharts.com/docs/options/states/
*************************************** */
states: {
hover: { filter: { type: 'darken' } },
active: { filter: { type: 'darken' } },
},
/** **************************************
* Fill
* https://apexcharts.com/docs/options/fill/
*************************************** */
fill: {
opacity: 1,
gradient: {
type: 'vertical',
shadeIntensity: 0,
opacityFrom: 0.4,
opacityTo: 0,
stops: [0, 100],
},
},
/** **************************************
* Data labels
* https://apexcharts.com/docs/options/datalabels/
*************************************** */
dataLabels: { enabled: false },
/** **************************************
* Stroke
* https://apexcharts.com/docs/options/stroke/
*************************************** */
stroke: { width: 2.5, curve: 'smooth', lineCap: 'round' },
/** **************************************
* Grid
* https://apexcharts.com/docs/options/grid/
*************************************** */
grid: {
strokeDashArray: 3,
borderColor: theme.vars.palette.divider,
padding: { top: 0, right: 0, bottom: 0 },
xaxis: { lines: { show: false } },
},
/** **************************************
* Axis
* https://apexcharts.com/docs/options/xaxis/
* https://apexcharts.com/docs/options/yaxis/
*************************************** */
xaxis: { axisBorder: { show: false }, axisTicks: { show: false } },
yaxis: { tickAmount: 5 },
/** **************************************
* Markers
* https://apexcharts.com/docs/options/markers/
*************************************** */
markers: {
size: 0,
strokeColors: theme.vars.palette.background.paper,
},
/** **************************************
* Tooltip
*************************************** */
tooltip: { theme: 'false', fillSeriesColor: false, x: { show: true } },
/** **************************************
* Legend
* https://apexcharts.com/docs/options/legend/
*************************************** */
legend: {
show: false,
position: 'top',
fontWeight: 500,
fontSize: '13px',
horizontalAlign: 'right',
markers: { shape: 'circle' },
labels: { colors: theme.vars.palette.text.primary },
itemMargin: { horizontal: 8, vertical: 8 },
},
/** **************************************
* plotOptions
*************************************** */
plotOptions: {
/**
* bar
* https://apexcharts.com/docs/options/plotoptions/bar/
*/
bar: { borderRadius: 4, columnWidth: '48%', borderRadiusApplication: 'end' },
/**
* pie + donut
* https://apexcharts.com/docs/options/plotoptions/pie/
*/
pie: {
donut: { labels: { show: true, value: { ...LABEL_VALUE }, total: { ...LABEL_TOTAL } } },
},
/**
* radialBar
* https://apexcharts.com/docs/options/plotoptions/radialbar/
*/
radialBar: {
hollow: { margin: -8, size: '100%' },
track: {
margin: -8,
strokeWidth: '50%',
background: varAlpha(theme.vars.palette.grey['500Channel'], 0.16),
},
dataLabels: { value: { ...LABEL_VALUE }, total: { ...LABEL_TOTAL } },
},
/**
* radar
* https://apexcharts.com/docs/options/plotoptions/radar/
*/
radar: {
polygons: {
fill: { colors: ['transparent'] },
strokeColors: theme.vars.palette.divider,
connectorColors: theme.vars.palette.divider,
},
},
/**
* polarArea
* https://apexcharts.com/docs/options/plotoptions/polararea/
*/
polarArea: {
rings: { strokeColor: theme.vars.palette.divider },
spokes: { connectorColors: theme.vars.palette.divider },
},
/**
* heatmap
* https://apexcharts.com/docs/options/plotoptions/heatmap/
*/
heatmap: { distributed: true },
},
/** **************************************
* Responsive
* https://apexcharts.com/docs/options/responsive/
*************************************** */
responsive: [
{
breakpoint: theme.breakpoints.values.sm, // sm ~ 600
options: { plotOptions: { bar: { borderRadius: 3, columnWidth: '80%' } } },
},
{
breakpoint: theme.breakpoints.values.md, // md ~ 900
options: { plotOptions: { bar: { columnWidth: '60%' } } },
},
],
};
};

View File

@@ -0,0 +1,18 @@
import { createClasses } from 'src/theme/create-classes';
// ----------------------------------------------------------------------
export const colorPreviewClasses = {
root: createClasses('color__preview__root'),
item: createClasses('color__preview__item'),
label: createClasses('color__preview__label'),
};
export const colorPickerClasses = {
root: createClasses('color__picker__root'),
item: {
root: createClasses('color__picker__item__root'),
container: createClasses('color__picker__item__container'),
icon: createClasses('color__picker__item__icon'),
},
};

View File

@@ -0,0 +1,179 @@
import type { Theme, SxProps } from '@mui/material/styles';
import { useCallback } from 'react';
import { varAlpha, mergeClasses } from 'minimal-shared/utils';
import ButtonBase from '@mui/material/ButtonBase';
import { styled, alpha as hexAlpha } from '@mui/material/styles';
import { Iconify } from '../iconify';
import { colorPickerClasses } from './classes';
// ----------------------------------------------------------------------
export type ColorPickerSlotProps = {
item?: React.ComponentProps<typeof ItemRoot>;
itemContainer?: React.ComponentProps<typeof ItemContainer>;
icon?: React.ComponentProps<typeof ItemIcon>;
};
export type ColorPickerProps = Omit<React.ComponentProps<'ul'>, 'onChange'> & {
sx?: SxProps<Theme>;
size?: number;
options?: string[];
limit?: 'auto' | number;
value?: string | string[];
variant?: 'circular' | 'rounded' | 'square';
onChange?: (value: string | string[]) => void;
slotProps?: ColorPickerSlotProps;
};
export function ColorPicker({
sx,
value,
onChange,
slotProps,
className,
size = 36,
options = [],
limit = 'auto',
variant = 'circular',
...other
}: ColorPickerProps) {
const isSingleSelect = typeof value === 'string';
const handleSelect = useCallback(
(color: string) => {
if (isSingleSelect) {
if (color !== value) {
onChange?.(color);
}
} else {
const selected = value as string[];
const newSelected = selected.includes(color)
? selected.filter((currentColor) => currentColor !== color)
: [...selected, color];
onChange?.(newSelected);
}
},
[onChange, value, isSingleSelect]
);
return (
<ColorPickerRoot
limit={limit}
className={mergeClasses([colorPickerClasses.root, className])}
sx={[
{
'--item-size': `${size}px`,
'--item-radius':
(variant === 'circular' && '50%') ||
(variant === 'rounded' && 'calc(var(--item-size) / 6)') ||
'0px',
},
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
{options.map((color) => {
const hasSelected = isSingleSelect ? value === color : (value as string[]).includes(color);
return (
<li key={color}>
<ItemRoot
aria-label={color}
onClick={() => handleSelect(color)}
className={colorPickerClasses.item.root}
{...slotProps?.item}
>
<ItemContainer
color={color}
hasSelected={hasSelected}
className={colorPickerClasses.item.container}
{...slotProps?.itemContainer}
>
<ItemIcon
color={color}
hasSelected={hasSelected}
icon="eva:checkmark-fill"
className={colorPickerClasses.item.icon}
{...slotProps?.icon}
/>
</ItemContainer>
</ItemRoot>
</li>
);
})}
</ColorPickerRoot>
);
}
// ----------------------------------------------------------------------
const ColorPickerRoot = styled('ul', {
shouldForwardProp: (prop: string) => !['limit', 'sx'].includes(prop),
})<Pick<ColorPickerProps, 'limit'>>(({ limit }) => ({
flexWrap: 'wrap',
flexDirection: 'row',
display: 'inline-flex',
'& > li': { display: 'inline-flex' },
...(typeof limit === 'number' && {
justifyContent: 'flex-end',
width: `calc(var(--item-size) * ${limit})`,
}),
}));
const ItemRoot = styled(ButtonBase)(() => ({
width: 'var(--item-size)',
height: 'var(--item-size)',
borderRadius: 'var(--item-radius)',
}));
const ItemContainer = styled('span', {
shouldForwardProp: (prop: string) => !['color', 'hasSelected', 'sx'].includes(prop),
})<{ color: string; hasSelected: boolean }>(({ color, theme }) => ({
alignItems: 'center',
display: 'inline-flex',
borderRadius: 'inherit',
justifyContent: 'center',
backgroundColor: color,
width: 'calc(var(--item-size) - 16px)',
height: 'calc(var(--item-size) - 16px)',
border: `solid 1px ${varAlpha(theme.vars.palette.grey['500Channel'], 0.16)}`,
transition: theme.transitions.create(['all'], {
duration: theme.transitions.duration.shortest,
}),
variants: [
{
props: { hasSelected: true },
style: {
width: 'calc(var(--item-size) - 8px)',
height: 'calc(var(--item-size) - 8px)',
outline: `solid 2px ${hexAlpha(color, 0.08)}`,
boxShadow: `4px 4px 8px 0 ${hexAlpha(color, 0.48)}`,
},
},
],
}));
const ItemIcon = styled(Iconify, {
shouldForwardProp: (prop: string) => !['color', 'hasSelected', 'sx'].includes(prop),
})<{ color: string; hasSelected: boolean }>(({ color, theme }) => ({
width: 0,
height: 0,
color: theme.palette.getContrastText(color),
transition: theme.transitions.create(['all'], {
duration: theme.transitions.duration.shortest,
}),
variants: [
{
props: { hasSelected: true },
style: {
width: 'calc(var(--item-size) / 2.4)',
height: 'calc(var(--item-size) / 2.4)',
},
},
],
}));

View File

@@ -0,0 +1,90 @@
import { varAlpha, mergeClasses } from 'minimal-shared/utils';
import { styled } from '@mui/material/styles';
import { colorPreviewClasses } from './classes';
// ----------------------------------------------------------------------
export type ColorPreviewSlotProps = {
item?: React.ComponentProps<typeof ItemRoot>;
label?: React.ComponentProps<typeof ItemLabel>;
};
export type ColorPreviewProps = React.ComponentProps<typeof ColorPreviewRoot> & {
limit?: number;
size?: number;
gap?: number;
colors: string[];
slotProps?: ColorPreviewSlotProps;
};
export function ColorPreview({
sx,
colors,
className,
slotProps,
gap = 6,
limit = 3,
size = 16,
...other
}: ColorPreviewProps) {
const colorsRange = colors.slice(0, limit);
const remainingColorCount = colors.length - limit;
return (
<ColorPreviewRoot
className={mergeClasses([colorPreviewClasses.root, className])}
sx={sx}
{...other}
>
{colorsRange.map((color, index) => (
<ItemRoot
key={color + index}
className={colorPreviewClasses.item}
{...slotProps?.item}
sx={[
{
'--item-color': color,
'--item-size': `${size}px`,
'--item-gap': `${-gap}px`,
},
...(Array.isArray(slotProps?.item?.sx)
? (slotProps.item?.sx ?? [])
: [slotProps?.item?.sx]),
]}
/>
))}
{colors.length > limit && (
<ItemLabel
className={colorPreviewClasses.label}
{...slotProps?.label}
>{`+${remainingColorCount}`}</ItemLabel>
)}
</ColorPreviewRoot>
);
}
// ----------------------------------------------------------------------
const ColorPreviewRoot = styled('ul')(() => ({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-end',
}));
const ItemRoot = styled('li')(({ theme }) => ({
borderRadius: '50%',
width: 'var(--item-size)',
height: 'var(--item-size)',
marginLeft: 'var(--item-gap)',
backgroundColor: 'var(--item-color)',
border: `solid 2px ${theme.vars.palette.background.paper}`,
boxShadow: `inset -1px 1px 2px ${varAlpha(theme.vars.palette.common.blackChannel, 0.24)}`,
}));
const ItemLabel = styled('li')(({ theme }) => ({
...theme.typography.subtitle2,
}));

View File

@@ -0,0 +1,5 @@
export * from './classes';
export * from './color-picker';
export * from './color-preview';

View File

@@ -0,0 +1,7 @@
import { createClasses } from 'src/theme/create-classes';
// ----------------------------------------------------------------------
export const iconifyClasses = {
root: createClasses('iconify__root'),
};

View File

@@ -0,0 +1,101 @@
export default {
'solar:pen-bold': {
body: '<path fill="currentColor" d="m11.4 18.161l7.396-7.396a10.3 10.3 0 0 1-3.326-2.234a10.3 10.3 0 0 1-2.235-3.327L5.839 12.6c-.577.577-.866.866-1.114 1.184a6.6 6.6 0 0 0-.749 1.211c-.173.364-.302.752-.56 1.526l-1.362 4.083a1.06 1.06 0 0 0 1.342 1.342l4.083-1.362c.775-.258 1.162-.387 1.526-.56q.647-.308 1.211-.749c.318-.248.607-.537 1.184-1.114m9.448-9.448a3.932 3.932 0 0 0-5.561-5.561l-.887.887l.038.111a8.75 8.75 0 0 0 2.092 3.32a8.75 8.75 0 0 0 3.431 2.13z"/>',
},
'solar:eye-bold': {
body: '<path fill="currentColor" d="M9.75 12a2.25 2.25 0 1 1 4.5 0a2.25 2.25 0 0 1-4.5 0"/><path fill="currentColor" fill-rule="evenodd" d="M2 12c0 1.64.425 2.191 1.275 3.296C4.972 17.5 7.818 20 12 20s7.028-2.5 8.725-4.704C21.575 14.192 22 13.639 22 12c0-1.64-.425-2.191-1.275-3.296C19.028 6.5 16.182 4 12 4S4.972 6.5 3.275 8.704C2.425 9.81 2 10.361 2 12m10-3.75a3.75 3.75 0 1 0 0 7.5a3.75 3.75 0 0 0 0-7.5" clip-rule="evenodd"/>',
},
'solar:share-bold': {
body: '<path fill="currentColor" fill-rule="evenodd" d="M13.803 5.333c0-1.84 1.5-3.333 3.348-3.333A3.34 3.34 0 0 1 20.5 5.333c0 1.841-1.5 3.334-3.349 3.334a3.35 3.35 0 0 1-2.384-.994l-4.635 3.156a3.34 3.34 0 0 1-.182 1.917l5.082 3.34a3.35 3.35 0 0 1 2.12-.753a3.34 3.34 0 0 1 3.348 3.334C20.5 20.507 19 22 17.151 22a3.34 3.34 0 0 1-3.348-3.333a3.3 3.3 0 0 1 .289-1.356L9.05 14a3.35 3.35 0 0 1-2.202.821A3.34 3.34 0 0 1 3.5 11.487a3.34 3.34 0 0 1 3.348-3.333c1.064 0 2.01.493 2.623 1.261l4.493-3.059a3.3 3.3 0 0 1-.161-1.023" clip-rule="evenodd"/>',
},
'solar:cart-3-bold': {
body: '<path fill="currentColor" fill-rule="evenodd" d="M10 2.25a1.75 1.75 0 0 0-1.582 1c-.684.006-1.216.037-1.692.223A3.25 3.25 0 0 0 5.3 4.563c-.367.493-.54 1.127-.776 1.998l-.047.17l-.513 2.964q-.277.191-.486.459c-.901 1.153-.472 2.87.386 6.301c.545 2.183.818 3.274 1.632 3.91C6.31 21 7.435 21 9.685 21h4.63c2.25 0 3.375 0 4.189-.635c.814-.636 1.086-1.727 1.632-3.91c.858-3.432 1.287-5.147.386-6.301a2.2 2.2 0 0 0-.487-.46l-.513-2.962l-.046-.17c-.237-.872-.41-1.506-.776-2a3.25 3.25 0 0 0-1.426-1.089c-.476-.186-1.009-.217-1.692-.222A1.75 1.75 0 0 0 14 2.25zm8.418 6.896l-.362-2.088c-.283-1.04-.386-1.367-.56-1.601a1.75 1.75 0 0 0-.768-.587c-.22-.086-.486-.111-1.148-.118A1.75 1.75 0 0 1 14 5.75h-4a1.75 1.75 0 0 1-1.58-.998c-.663.007-.928.032-1.148.118a1.75 1.75 0 0 0-.768.587c-.174.234-.277.56-.56 1.6l-.362 2.089C6.58 9 7.91 9 9.685 9h4.63c1.775 0 3.105 0 4.103.146M8 12.25a.75.75 0 0 1 .75.75v4a.75.75 0 0 1-1.5 0v-4a.75.75 0 0 1 .75-.75m8.75.75a.75.75 0 0 0-1.5 0v4a.75.75 0 0 0 1.5 0zM12 12.25a.75.75 0 0 1 .75.75v4a.75.75 0 0 1-1.5 0v-4a.75.75 0 0 1 .75-.75" clip-rule="evenodd"/>',
},
'solar:restart-bold': {
body: '<path fill="currentColor" d="M18.258 3.508a.75.75 0 0 1 .463.693v4.243a.75.75 0 0 1-.75.75h-4.243a.75.75 0 0 1-.53-1.28L14.8 6.31a7.25 7.25 0 1 0 4.393 5.783a.75.75 0 0 1 1.488-.187A8.75 8.75 0 1 1 15.93 5.18l1.51-1.51a.75.75 0 0 1 .817-.162"/>',
},
'solar:eye-closed-bold': {
body: '<path fill="currentColor" fill-rule="evenodd" d="M1.606 6.08a1 1 0 0 1 1.313.526L2 7l.92-.394v-.001l.003.009l.021.045l.094.194c.086.172.219.424.4.729a13.4 13.4 0 0 0 1.67 2.237a12 12 0 0 0 .59.592C7.18 11.8 9.251 13 12 13a8.7 8.7 0 0 0 3.22-.602c1.227-.483 2.254-1.21 3.096-1.998a13 13 0 0 0 2.733-3.725l.027-.058l.005-.011a1 1 0 0 1 1.838.788L22 7l.92.394l-.003.005l-.004.008l-.011.026l-.04.087a14 14 0 0 1-.741 1.348a15.4 15.4 0 0 1-1.711 2.256l.797.797a1 1 0 0 1-1.414 1.415l-.84-.84a12 12 0 0 1-1.897 1.256l.782 1.202a1 1 0 1 1-1.676 1.091l-.986-1.514c-.679.208-1.404.355-2.176.424V16.5a1 1 0 0 1-2 0v-1.544c-.775-.07-1.5-.217-2.177-.425l-.985 1.514a1 1 0 0 1-1.676-1.09l.782-1.203c-.7-.37-1.332-.8-1.897-1.257l-.84.84a1 1 0 0 1-1.414-1.414l.797-.797a15.4 15.4 0 0 1-1.87-2.519a14 14 0 0 1-.591-1.107l-.033-.072l-.01-.021l-.002-.007l-.001-.002v-.001C1.08 7.395 1.08 7.394 2 7l-.919.395a1 1 0 0 1 .525-1.314" clip-rule="evenodd"/>',
},
'solar:check-circle-bold': {
body: '<path fill="currentColor" fill-rule="evenodd" d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2s10 4.477 10 10m-5.97-3.03a.75.75 0 0 1 0 1.06l-5 5a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 1 1 1.06-1.06l1.47 1.47l2.235-2.235L14.97 8.97a.75.75 0 0 1 1.06 0" clip-rule="evenodd"/>',
},
'solar:trash-bin-trash-bold': {
body: '<path fill="currentColor" d="M3 6.386c0-.484.345-.877.771-.877h2.665c.529-.016.996-.399 1.176-.965l.03-.1l.115-.391c.07-.24.131-.45.217-.637c.338-.739.964-1.252 1.687-1.383c.184-.033.378-.033.6-.033h3.478c.223 0 .417 0 .6.033c.723.131 1.35.644 1.687 1.383c.086.187.147.396.218.637l.114.391l.03.1c.18.566.74.95 1.27.965h2.57c.427 0 .772.393.772.877s-.345.877-.771.877H3.77c-.425 0-.77-.393-.77-.877"/><path fill="currentColor" fill-rule="evenodd" d="M11.596 22h.808c2.783 0 4.174 0 5.08-.886c.904-.886.996-2.339 1.181-5.245l.267-4.188c.1-1.577.15-2.366-.303-2.865c-.454-.5-1.22-.5-2.753-.5H8.124c-1.533 0-2.3 0-2.753.5s-.404 1.288-.303 2.865l.267 4.188c.185 2.906.277 4.36 1.182 5.245c.905.886 2.296.886 5.079.886m-1.35-9.811c-.04-.434-.408-.75-.82-.707c-.413.043-.713.43-.672.864l.5 5.263c.04.434.408.75.82.707c.413-.043.713-.43.672-.864zm4.329-.707c.412.043.713.43.671.864l-.5 5.263c-.04.434-.409.75-.82.707c-.413-.043-.713-.43-.672-.864l.5-5.263c.04-.434.409-.75.82-.707" clip-rule="evenodd"/>',
},
'solar:chat-round-dots-bold': {
body: '<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M22 12C22 17.523 17.523 22 12 22C10.4551 22.002 8.93095 21.6446 7.548 20.956C7.19414 20.7727 6.78538 20.7254 6.399 20.823L4.173 21.419C3.95267 21.4778 3.72075 21.4776 3.50053 21.4184C3.2803 21.3593 3.07951 21.2432 2.91831 21.0819C2.75712 20.9206 2.64119 20.7197 2.58216 20.4995C2.52312 20.2792 2.52307 20.0473 2.582 19.827L3.177 17.601C3.28 17.216 3.221 16.809 3.043 16.453C2.376 15.112 2 13.6 2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12ZM15.2929 12.7071C15.1054 12.5196 15 12.2652 15 12C15 11.7348 15.1054 11.4804 15.2929 11.2929C15.4804 11.1054 15.7348 11 16 11C16.2652 11 16.5196 11.1054 16.7071 11.2929C16.8946 11.4804 17 11.7348 17 12C17 12.2652 16.8946 12.5196 16.7071 12.7071C16.5196 12.8946 16.2652 13 16 13C15.7348 13 15.4804 12.8946 15.2929 12.7071ZM11.2929 12.7071C11.1054 12.5196 11 12.2652 11 12C11 11.7348 11.1054 11.4804 11.2929 11.2929C11.4804 11.1054 11.7348 11 12 11C12.2652 11 12.5196 11.1054 12.7071 11.2929C12.8946 11.4804 13 11.7348 13 12C13 12.2652 12.8946 12.5196 12.7071 12.7071C12.5196 12.8946 12.2652 13 12 13C11.7348 13 11.4804 12.8946 11.2929 12.7071ZM7.29289 12.7071C7.10536 12.5196 7 12.2652 7 12C7 11.7348 7.10536 11.4804 7.29289 11.2929C7.48043 11.1054 7.73478 11 8 11C8.26522 11 8.51957 11.1054 8.70711 11.2929C8.89464 11.4804 9 11.7348 9 12C9 12.2652 8.89464 12.5196 8.70711 12.7071C8.51957 12.8946 8.26522 13 8 13C7.73478 13 7.48043 12.8946 7.29289 12.7071Z" />',
},
'solar:clock-circle-outline': {
body: '<path fill="currentColor" fill-rule="evenodd" d="M12 2.75a9.25 9.25 0 1 0 0 18.5a9.25 9.25 0 0 0 0-18.5M1.25 12C1.25 6.063 6.063 1.25 12 1.25S22.75 6.063 22.75 12S17.937 22.75 12 22.75S1.25 17.937 1.25 12M12 7.25a.75.75 0 0 1 .75.75v3.69l2.28 2.28a.75.75 0 1 1-1.06 1.06l-2.5-2.5a.75.75 0 0 1-.22-.53V8a.75.75 0 0 1 .75-.75" clip-rule="evenodd"/>',
},
'solar:bell-bing-bold-duotone': {
body: '<path fill="currentColor" d="M18.75 9v.704c0 .845.24 1.671.692 2.374l1.108 1.723c1.011 1.574.239 3.713-1.52 4.21a25.8 25.8 0 0 1-14.06 0c-1.759-.497-2.531-2.636-1.52-4.21l1.108-1.723a4.4 4.4 0 0 0 .693-2.374V9c0-3.866 3.022-7 6.749-7s6.75 3.134 6.75 7" opacity="0.4"/><path fill="currentColor" d="M12.75 6a.75.75 0 0 0-1.5 0v4a.75.75 0 0 0 1.5 0zM7.243 18.545a5.002 5.002 0 0 0 9.513 0c-3.145.59-6.367.59-9.513 0"/>',
},
'solar:home-angle-bold-duotone': {
body: '<path fill="currentColor" d="M13.106 22h-2.212c-3.447 0-5.17 0-6.345-1.012s-1.419-2.705-1.906-6.093l-.279-1.937c-.38-2.637-.57-3.956-.029-5.083s1.691-1.813 3.992-3.183l1.385-.825C9.8 2.622 10.846 2 12 2s2.199.622 4.288 1.867l1.385.825c2.3 1.37 3.451 2.056 3.992 3.183s.35 2.446-.03 5.083l-.278 1.937c-.487 3.388-.731 5.081-1.906 6.093S16.553 22 13.106 22" opacity="0.4"/><path fill="currentColor" d="M8.25 18a.75.75 0 0 1 .75-.75h6a.75.75 0 0 1 0 1.5H9a.75.75 0 0 1-.75-.75"/>',
},
'solar:settings-bold-duotone': {
body: '<path fill="currentColor" fill-rule="evenodd" d="M14.279 2.152C13.909 2 13.439 2 12.5 2s-1.408 0-1.779.152a2 2 0 0 0-1.09 1.083c-.094.223-.13.484-.145.863a1.62 1.62 0 0 1-.796 1.353a1.64 1.64 0 0 1-1.579.008c-.338-.178-.583-.276-.825-.308a2.03 2.03 0 0 0-1.49.396c-.318.242-.553.646-1.022 1.453c-.47.807-.704 1.21-.757 1.605c-.07.526.074 1.058.4 1.479c.148.192.357.353.68.555c.477.297.783.803.783 1.361s-.306 1.064-.782 1.36c-.324.203-.533.364-.682.556a2 2 0 0 0-.399 1.479c.053.394.287.798.757 1.605s.704 1.21 1.022 1.453c.424.323.96.465 1.49.396c.242-.032.487-.13.825-.308a1.64 1.64 0 0 1 1.58.008c.486.28.774.795.795 1.353c.015.38.051.64.145.863c.204.49.596.88 1.09 1.083c.37.152.84.152 1.779.152s1.409 0 1.779-.152a2 2 0 0 0 1.09-1.083c.094-.223.13-.483.145-.863c.02-.558.309-1.074.796-1.353a1.64 1.64 0 0 1 1.579-.008c.338.178.583.276.825.308c.53.07 1.066-.073 1.49-.396c.318-.242.553-.646 1.022-1.453c.47-.807.704-1.21.757-1.605a2 2 0 0 0-.4-1.479c-.148-.192-.357-.353-.68-.555c-.477-.297-.783-.803-.783-1.361s.306-1.064.782-1.36c.324-.203.533-.364.682-.556a2 2 0 0 0 .399-1.479c-.053-.394-.287-.798-.757-1.605s-.704-1.21-1.022-1.453a2.03 2.03 0 0 0-1.49-.396c-.242.032-.487.13-.825.308a1.64 1.64 0 0 1-1.58-.008a1.62 1.62 0 0 1-.795-1.353c-.015-.38-.051-.64-.145-.863a2 2 0 0 0-1.09-1.083" clip-rule="evenodd" opacity="0.4"/><path fill="currentColor" d="M15.523 12c0 1.657-1.354 3-3.023 3s-3.023-1.343-3.023-3S10.83 9 12.5 9s3.023 1.343 3.023 3"/>',
},
'solar:shield-keyhole-bold-duotone': {
body: '<path fill="currentColor" d="M3 10.417c0-3.198 0-4.797.378-5.335c.377-.537 1.88-1.052 4.887-2.081l.573-.196C10.405 2.268 11.188 2 12 2s1.595.268 3.162.805l.573.196c3.007 1.029 4.51 1.544 4.887 2.081C21 5.62 21 7.22 21 10.417v1.574c0 5.638-4.239 8.375-6.899 9.536C13.38 21.842 13.02 22 12 22s-1.38-.158-2.101-.473C7.239 20.365 3 17.63 3 11.991z" opacity="0.4"/><path fill="currentColor" d="M13.5 15a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1.401A2.999 2.999 0 0 1 12 8a3 3 0 0 1 1.5 5.599z"/>',
},
'eva:more-vertical-fill': {
body: '<circle cx="12" cy="12" r="2" fill="currentColor"/><circle cx="12" cy="5" r="2" fill="currentColor"/><circle cx="12" cy="19" r="2" fill="currentColor"/>',
},
'eva:search-fill': {
body: '<path fill="currentColor" d="m20.71 19.29l-3.4-3.39A7.92 7.92 0 0 0 19 11a8 8 0 1 0-8 8a7.92 7.92 0 0 0 4.9-1.69l3.39 3.4a1 1 0 0 0 1.42 0a1 1 0 0 0 0-1.42M5 11a6 6 0 1 1 6 6a6 6 0 0 1-6-6"/>',
},
'eva:done-all-fill': {
body: '<path fill="currentColor" d="M16.62 6.21a1 1 0 0 0-1.41.17l-7 9l-3.43-4.18a1 1 0 1 0-1.56 1.25l4.17 5.18a1 1 0 0 0 .78.37a1 1 0 0 0 .83-.38l7.83-10a1 1 0 0 0-.21-1.41m5 0a1 1 0 0 0-1.41.17l-7 9l-.61-.75l-1.26 1.62l1.1 1.37a1 1 0 0 0 .78.37a1 1 0 0 0 .78-.38l7.83-10a1 1 0 0 0-.21-1.4"/><path fill="currentColor" d="M8.71 13.06L10 11.44l-.2-.24a1 1 0 0 0-1.43-.2a1 1 0 0 0-.15 1.41Z"/>',
},
'eva:checkmark-fill': {
body: '<path fill="currentColor" d="M9.86 18a1 1 0 0 1-.73-.32l-4.86-5.17a1 1 0 1 1 1.46-1.37l4.12 4.39l8.41-9.2a1 1 0 1 1 1.48 1.34l-9.14 10a1 1 0 0 1-.73.33Z"/>',
},
'eva:trending-down-fill': {
body: '<path fill="currentColor" d="M21 12a1 1 0 0 0-2 0v2.3l-4.24-5a1 1 0 0 0-1.27-.21L9.22 11.7L4.77 6.36a1 1 0 1 0-1.54 1.28l5 6a1 1 0 0 0 1.28.22l4.28-2.57l4 4.71H15a1 1 0 0 0 0 2h5a1.1 1.1 0 0 0 .36-.07l.14-.08a1 1 0 0 0 .15-.09a.8.8 0 0 0 .14-.17a1 1 0 0 0 .09-.14a.6.6 0 0 0 .05-.17A.8.8 0 0 0 21 17Z"/>',
},
'eva:trending-up-fill': {
body: '<path fill="currentColor" d="M21 7a.8.8 0 0 0 0-.21a.6.6 0 0 0-.05-.17a1 1 0 0 0-.09-.14a.8.8 0 0 0-.14-.17l-.12-.07a.7.7 0 0 0-.19-.1h-.2A.7.7 0 0 0 20 6h-5a1 1 0 0 0 0 2h2.83l-4 4.71l-4.32-2.57a1 1 0 0 0-1.28.22l-5 6a1 1 0 0 0 .13 1.41A1 1 0 0 0 4 18a1 1 0 0 0 .77-.36l4.45-5.34l4.27 2.56a1 1 0 0 0 1.27-.21L19 9.7V12a1 1 0 0 0 2 0z"/>',
},
'eva:arrow-ios-forward-fill': {
body: '<path fill="currentColor" d="M10 19a1 1 0 0 1-.64-.23a1 1 0 0 1-.13-1.41L13.71 12L9.39 6.63a1 1 0 0 1 .15-1.41a1 1 0 0 1 1.46.15l4.83 6a1 1 0 0 1 0 1.27l-5 6A1 1 0 0 1 10 19"/>',
},
'eva:arrow-ios-downward-fill': {
body: '<path fill="currentColor" d="M12 16a1 1 0 0 1-.64-.23l-6-5a1 1 0 1 1 1.28-1.54L12 13.71l5.36-4.32a1 1 0 0 1 1.41.15a1 1 0 0 1-.14 1.46l-6 4.83A1 1 0 0 1 12 16"/>',
},
'eva:arrow-ios-upward-fill': {
body: '<path fill="currentColor" d="M18 15a1 1 0 0 1-.64-.23L12 10.29l-5.37 4.32a1 1 0 0 1-1.41-.15a1 1 0 0 1 .15-1.41l6-4.83a1 1 0 0 1 1.27 0l6 5a1 1 0 0 1 .13 1.41A1 1 0 0 1 18 15"/>',
},
'ic:round-filter-list': {
body: '<path fill="currentColor" d="M11 18h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1M3 7c0 .55.45 1 1 1h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1m4 6h10c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1"/>',
},
'mingcute:add-line': {
body: '<g fill="none"><path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M11 20a1 1 0 1 0 2 0v-7h7a1 1 0 1 0 0-2h-7V4a1 1 0 1 0-2 0v7H4a1 1 0 1 0 0 2h7z"/></g>',
},
'mingcute:close-line': {
body: '<g fill="none" fill-rule="evenodd"><path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="m12 13.414l5.657 5.657a1 1 0 0 0 1.414-1.414L13.414 12l5.657-5.657a1 1 0 0 0-1.414-1.414L12 10.586L6.343 4.929A1 1 0 0 0 4.93 6.343L10.586 12l-5.657 5.657a1 1 0 1 0 1.414 1.414z"/></g>',
},
'carbon:chevron-sort': {
body: '<path fill="currentColor" d="m16 28l-7-7l1.41-1.41L16 25.17l5.59-5.58L23 21zm0-24l7 7l-1.41 1.41L16 6.83l-5.59 5.58L9 11z"/>',
},
'socials:linkedin': {
body: '<path fill="var(--color, #0A66C2)" d="M5.14678 2.52608C5.85835 2.65326 6.45746 3.09997 6.77593 3.74217C6.88945 3.97025 6.95882 4.20149 6.99245 4.46426C7.01558 4.64609 7.00822 4.94775 6.97669 5.12433C6.82323 5.99567 6.18418 6.68832 5.33071 6.9122C5.1699 6.95424 5.03326 6.97316 4.84722 6.98157C3.96327 7.01731 3.14449 6.52856 2.75034 5.72869C2.44342 5.10646 2.44342 4.37597 2.74928 3.75268C3.08983 3.06003 3.73729 2.60281 4.51613 2.50822C4.64962 2.4914 5.00909 2.50191 5.14678 2.52608ZM17.2025 8.64541C17.768 8.70007 18.324 8.84932 18.7844 9.0711C19.1407 9.24242 19.5464 9.50414 19.9154 9.80264C20.3274 10.1358 20.6175 10.5436 20.9549 11.2626C21.2281 11.8449 21.3784 12.341 21.4562 12.9128C21.473 13.0347 21.4741 13.3626 21.4772 17.2715L21.4804 21.5H19.5622H17.644V17.9442C17.644 15.5436 17.6408 14.3538 17.6335 14.2791C17.5452 13.3899 17.1374 12.753 16.4437 12.4188C16.2797 12.3399 16.1704 12.3 15.998 12.2558C15.3159 12.0803 14.5097 12.1791 13.9095 12.5102C13.3514 12.8182 12.931 13.4499 12.7733 14.2161C12.7113 14.5188 12.7145 14.3096 12.7145 18.0598V21.5H10.8015H8.88858V15.1778V8.85563H10.6912H12.4938V9.76585V10.675L12.5715 10.571C12.7344 10.3513 13.0351 10.037 13.2579 9.85204C14.1187 9.13626 15.2244 8.69271 16.2944 8.6349C16.4952 8.62439 17.0491 8.6307 17.2025 8.64541ZM6.72338 15.1778V21.5H4.76313H2.80289V15.1778V8.85563H4.76313H6.72338V15.1778Z" />',
},
'socials:facebook': {
body: '<path fill="var(--color, #1877F2)" d="M14 13.5H16.5L17.5 9.5H14V7.5C14 6.47062 14 5.5 16 5.5H17.5V2.1401C17.1743 2.09685 15.943 2 14.6429 2C11.9284 2 10 3.65686 10 6.69971V9.5H7V13.5H10V22H14V13.5Z" />',
},
'socials:github': {
body: '<path fill="var(--color, var(--palette-text-primary))" d="M12.001 2c-5.525 0-10 4.475-10 10a9.994 9.994 0 0 0 6.837 9.488c.5.087.688-.213.688-.476c0-.237-.013-1.024-.013-1.862c-2.512.463-3.162-.612-3.362-1.175c-.113-.288-.6-1.175-1.025-1.413c-.35-.187-.85-.65-.013-.662c.788-.013 1.35.725 1.538 1.025c.9 1.512 2.337 1.087 2.912.825c.088-.65.35-1.087.638-1.337c-2.225-.25-4.55-1.113-4.55-4.938c0-1.088.387-1.987 1.025-2.687c-.1-.25-.45-1.275.1-2.65c0 0 .837-.263 2.75 1.024a9.28 9.28 0 0 1 2.5-.337c.85 0 1.7.112 2.5.337c1.913-1.3 2.75-1.024 2.75-1.024c.55 1.375.2 2.4.1 2.65c.637.7 1.025 1.587 1.025 2.687c0 3.838-2.337 4.688-4.562 4.938c.362.312.675.912.675 1.85c0 1.337-.013 2.412-.013 2.75c0 .262.188.574.688.474A10.016 10.016 0 0 0 22 12c0-5.525-4.475-10-10-10" />',
},
'socials:twitter': {
body: '<path fill="var(--color, var(--palette-text-primary))" d="M17.7242 3H20.779L14.1069 10.624L21.956 21H15.8117L10.9959 14.7087L5.49201 21H2.43288L9.56798 12.8438L2.04346 3H8.34346L12.692 8.75048L17.7242 3ZM16.6511 19.174H18.343L7.42182 4.73077H5.60451L16.6511 19.174Z" />',
},
'socials:google': {
body: '<path fill="var(--color, #FFC107)" d="M21.8055 10.0415H21V10H12V14H17.6515C16.827 16.3285 14.6115 18 12 18C8.6865 18 6 15.3135 6 12C6 8.6865 8.6865 6 12 6C13.5295 6 14.921 6.577 15.9805 7.5195L18.809 4.691C17.023 3.0265 14.634 2 12 2C6.4775 2 2 6.4775 2 12C2 17.5225 6.4775 22 12 22C17.5225 22 22 17.5225 22 12C22 11.3295 21.931 10.675 21.8055 10.0415Z" /> <path fill="var(--color, #FF3D00)" d="M3.15332 7.3455L6.43882 9.755C7.32782 7.554 9.48082 6 12.0003 6C13.5298 6 14.9213 6.577 15.9808 7.5195L18.8093 4.691C17.0233 3.0265 14.6343 2 12.0003 2C8.15932 2 4.82832 4.1685 3.15332 7.3455Z" /> <path fill="var(--color, #4CAF50)" d="M12.0002 22C14.5832 22 16.9302 21.0115 18.7047 19.404L15.6097 16.785C14.5719 17.5742 13.3039 18.0011 12.0002 18C9.39916 18 7.19066 16.3415 6.35866 14.027L3.09766 16.5395C4.75266 19.778 8.11366 22 12.0002 22Z" /> <path fill="var(--color, #1976D2)" d="M21.8055 10.0415H21V10H12V14H17.6515C17.2571 15.1082 16.5467 16.0766 15.608 16.7855L15.6095 16.7845L18.7045 19.4035C18.4855 19.6025 22 17 22 12C22 11.3295 21.931 10.675 21.8055 10.0415Z" />',
},
'custom:menu-duotone': {
body: '<path fill="currentColor" opacity="0.4" d="M15.7798 4.5H5.2202C4.27169 4.5 3.5 5.06057 3.5 5.75042C3.5 6.43943 4.27169 7 5.2202 7H15.7798C16.7283 7 17.5 6.43943 17.5 5.75042C17.5 5.06054 16.7283 4.5 15.7798 4.5Z" ></path> <path fill="currentColor" d="M18.7798 10.75H8.2202C7.27169 10.75 6.5 11.3106 6.5 12.0004C6.5 12.6894 7.27169 13.25 8.2202 13.25H18.7798C19.7283 13.25 20.5 12.6894 20.5 12.0004C20.5 11.3105 19.7283 10.75 18.7798 10.75Z" ></path> <path fill="currentColor" d="M15.7798 17H5.2202C4.27169 17 3.5 17.5606 3.5 18.2504C3.5 18.9394 4.27169 19.5 5.2202 19.5H15.7798C16.7283 19.5 17.5 18.9394 17.5 18.2504C17.5 17.5606 16.7283 17 15.7798 17Z" ></path>',
},
};

View File

@@ -0,0 +1,58 @@
import type { IconProps } from '@iconify/react';
import { useId } from 'react';
import { Icon } from '@iconify/react';
import { mergeClasses } from 'minimal-shared/utils';
import { styled } from '@mui/material/styles';
import { iconifyClasses } from './classes';
import { allIconNames, registerIcons } from './register-icons';
import type { IconifyName } from './register-icons';
// ----------------------------------------------------------------------
export type IconifyProps = React.ComponentProps<typeof IconRoot> &
Omit<IconProps, 'icon'> & {
icon: IconifyName;
};
export function Iconify({ className, icon, width = 20, height, sx, ...other }: IconifyProps) {
const id = useId();
if (!allIconNames.includes(icon)) {
console.warn(
[
`Icon "${icon}" is currently loaded online, which may cause flickering effects.`,
`To ensure a smoother experience, please register your icon collection for offline use.`,
`More information is available at: https://docs.minimals.cc/icons/`,
].join('\n')
);
}
registerIcons();
return (
<IconRoot
ssr
id={id}
icon={icon}
className={mergeClasses([iconifyClasses.root, className])}
sx={[
{
width,
flexShrink: 0,
height: height ?? width,
display: 'inline-flex',
},
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
/>
);
}
// ----------------------------------------------------------------------
const IconRoot = styled(Icon)``;

View File

@@ -0,0 +1,5 @@
export * from './classes';
export * from './iconify';
export * from './register-icons';

View File

@@ -0,0 +1,51 @@
import type { IconifyJSON } from '@iconify/react';
import { addCollection } from '@iconify/react';
import allIcons from './icon-sets';
// ----------------------------------------------------------------------
export const iconSets = Object.entries(allIcons).reduce((acc, [key, value]) => {
const [prefix, iconName] = key.split(':');
const existingPrefix = acc.find((item) => item.prefix === prefix);
if (existingPrefix) {
existingPrefix.icons[iconName] = value;
} else {
acc.push({
prefix,
icons: {
[iconName]: value,
},
});
}
return acc;
}, [] as IconifyJSON[]);
export const allIconNames = Object.keys(allIcons) as IconifyName[];
export type IconifyName = keyof typeof allIcons;
// ----------------------------------------------------------------------
let areIconsRegistered = false;
export function registerIcons() {
if (areIconsRegistered) {
return;
}
iconSets.forEach((iconSet) => {
const iconSetConfig = {
...iconSet,
width: (iconSet.prefix === 'carbon' && 32) || 24,
height: (iconSet.prefix === 'carbon' && 32) || 24,
};
addCollection(iconSetConfig);
});
areIconsRegistered = true;
}

View File

@@ -0,0 +1,8 @@
import { createClasses } from 'src/theme/create-classes';
// ----------------------------------------------------------------------
export const labelClasses = {
root: createClasses('label__root'),
icon: createClasses('label__icon'),
};

View File

@@ -0,0 +1,7 @@
export * from './label';
export * from './styles';
export * from './classes';
export type * from './types';

View File

@@ -0,0 +1,38 @@
import { upperFirst } from 'es-toolkit';
import { mergeClasses } from 'minimal-shared/utils';
import { labelClasses } from './classes';
import { LabelRoot, LabelIcon } from './styles';
import type { LabelProps } from './types';
// ----------------------------------------------------------------------
export function Label({
sx,
endIcon,
children,
startIcon,
className,
disabled,
variant = 'soft',
color = 'default',
...other
}: LabelProps) {
return (
<LabelRoot
color={color}
variant={variant}
disabled={disabled}
className={mergeClasses([labelClasses.root, className])}
sx={sx}
{...other}
>
{startIcon && <LabelIcon className={labelClasses.icon}>{startIcon}</LabelIcon>}
{typeof children === 'string' ? upperFirst(children) : children}
{endIcon && <LabelIcon className={labelClasses.icon}>{endIcon}</LabelIcon>}
</LabelRoot>
);
}

View File

@@ -0,0 +1,116 @@
import type { CSSObject } from '@mui/material/styles';
import { varAlpha } from 'minimal-shared/utils';
import { styled } from '@mui/material/styles';
import type { LabelProps } from './types';
// ----------------------------------------------------------------------
export const LabelRoot = styled('span', {
shouldForwardProp: (prop: string) => !['color', 'variant', 'disabled', 'sx'].includes(prop),
})<LabelProps>(({ color, variant, disabled, theme }) => {
const defaultStyles: CSSObject = {
...(color === 'default' && {
/**
* @variant filled
*/
...(variant === 'filled' && {
color: theme.vars.palette.common.white,
backgroundColor: theme.vars.palette.text.primary,
...theme.applyStyles('dark', {
color: theme.vars.palette.grey[800],
}),
}),
/**
* @variant outlined
*/
...(variant === 'outlined' && {
backgroundColor: 'transparent',
color: theme.vars.palette.text.primary,
border: `2px solid ${theme.vars.palette.text.primary}`,
}),
/**
* @variant soft
*/
...(variant === 'soft' && {
color: theme.vars.palette.text.secondary,
backgroundColor: varAlpha(theme.vars.palette.grey['500Channel'], 0.16),
}),
/**
* @variant inverted
*/
...(variant === 'inverted' && {
color: theme.vars.palette.grey[800],
backgroundColor: theme.vars.palette.grey[300],
}),
}),
};
const colorStyles: CSSObject = {
...(color &&
color !== 'default' && {
/**
* @variant filled
*/
...(variant === 'filled' && {
color: theme.vars.palette[color].contrastText,
backgroundColor: theme.vars.palette[color].main,
}),
/**
* @variant outlined
*/
...(variant === 'outlined' && {
backgroundColor: 'transparent',
color: theme.vars.palette[color].main,
border: `2px solid ${theme.vars.palette[color].main}`,
}),
/**
* @variant soft
*/
...(variant === 'soft' && {
color: theme.vars.palette[color].dark,
backgroundColor: varAlpha(theme.vars.palette[color].mainChannel, 0.16),
...theme.applyStyles('dark', {
color: theme.vars.palette[color].light,
}),
}),
/**
* @variant inverted
*/
...(variant === 'inverted' && {
color: theme.vars.palette[color].darker,
backgroundColor: theme.vars.palette[color].lighter,
}),
}),
};
return {
height: 24,
minWidth: 24,
lineHeight: 0,
flexShrink: 0,
cursor: 'default',
alignItems: 'center',
whiteSpace: 'nowrap',
display: 'inline-flex',
gap: theme.spacing(0.75),
justifyContent: 'center',
padding: theme.spacing(0, 0.75),
fontSize: theme.typography.pxToRem(12),
fontWeight: theme.typography.fontWeightBold,
borderRadius: theme.shape.borderRadius * 0.75,
transition: theme.transitions.create(['all'], { duration: theme.transitions.duration.shorter }),
...defaultStyles,
...colorStyles,
...(disabled && { opacity: 0.48, pointerEvents: 'none' }),
};
});
export const LabelIcon = styled('span')({
width: 16,
height: 16,
flexShrink: 0,
'& svg, img': { width: '100%', height: '100%', objectFit: 'cover' },
});

View File

@@ -0,0 +1,23 @@
import type { Theme, SxProps } from '@mui/material/styles';
// ----------------------------------------------------------------------
export type LabelColor =
| 'default'
| 'primary'
| 'secondary'
| 'info'
| 'success'
| 'warning'
| 'error';
export type LabelVariant = 'filled' | 'outlined' | 'soft' | 'inverted';
export interface LabelProps extends React.ComponentProps<'span'> {
sx?: SxProps<Theme>;
disabled?: boolean;
color?: LabelColor;
variant?: LabelVariant;
endIcon?: React.ReactNode;
startIcon?: React.ReactNode;
}

View File

@@ -0,0 +1,7 @@
import { createClasses } from 'src/theme/create-classes';
// ----------------------------------------------------------------------
export const logoClasses = {
root: createClasses('logo__root'),
};

View File

@@ -0,0 +1,3 @@
export * from './logo';
export * from './classes';

View File

@@ -0,0 +1,197 @@
import type { LinkProps } from '@mui/material/Link';
import { useId } from 'react';
import { mergeClasses } from 'minimal-shared/utils';
import Link from '@mui/material/Link';
import { styled, useTheme } from '@mui/material/styles';
import { RouterLink } from 'src/routes/components';
import { logoClasses } from './classes';
// ----------------------------------------------------------------------
export type LogoProps = LinkProps & {
isSingle?: boolean;
disabled?: boolean;
};
export function Logo({
sx,
disabled,
className,
href = '/',
isSingle = true,
...other
}: LogoProps) {
const theme = useTheme();
const gradientId = useId();
const TEXT_PRIMARY = theme.vars.palette.text.primary;
const PRIMARY_LIGHT = theme.vars.palette.primary.light;
const PRIMARY_MAIN = theme.vars.palette.primary.main;
const PRIMARY_DARKER = theme.vars.palette.primary.dark;
const singleLogo = (
<svg
width="100%"
height="100%"
viewBox="0 0 512 512"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient
id={`${gradientId}-1`}
x1="152"
y1="167.79"
x2="65.523"
y2="259.624"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={PRIMARY_DARKER} />
<stop offset="1" stopColor={PRIMARY_MAIN} />
</linearGradient>
<linearGradient
id={`${gradientId}-2`}
x1="86"
y1="128"
x2="86"
y2="384"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={PRIMARY_LIGHT} />
<stop offset="1" stopColor={PRIMARY_MAIN} />
</linearGradient>
<linearGradient
id={`${gradientId}-3`}
x1="402"
y1="288"
x2="402"
y2="384"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={PRIMARY_LIGHT} />
<stop offset="1" stopColor={PRIMARY_MAIN} />
</linearGradient>
</defs>
<path
fill={`url(#${`${gradientId}-1`})`}
d="M86.352 246.358C137.511 214.183 161.836 245.017 183.168 285.573C165.515 317.716 153.837 337.331 148.132 344.418C137.373 357.788 125.636 367.911 111.202 373.752C80.856 388.014 43.132 388.681 14 371.048L86.352 246.358Z"
/>
<path
fill={`url(#${`${gradientId}-2`})`}
fillRule="evenodd"
clipRule="evenodd"
d="M444.31 229.726C398.04 148.77 350.21 72.498 295.267 184.382C287.751 198.766 282.272 226.719 270 226.719V226.577C257.728 226.577 252.251 198.624 244.735 184.24C189.79 72.356 141.96 148.628 95.689 229.584C92.207 235.69 88.862 241.516 86 246.58C192.038 179.453 183.11 382.247 270 383.858V384C356.891 382.389 347.962 179.595 454 246.72C451.139 241.658 447.794 235.832 444.31 229.726Z"
/>
<path
fill={`url(#${`${gradientId}-3`})`}
fillRule="evenodd"
clipRule="evenodd"
d="M450 384C476.509 384 498 362.509 498 336C498 309.491 476.509 288 450 288C423.491 288 402 309.491 402 336C402 362.509 423.491 384 450 384Z"
/>
</svg>
);
const fullLogo = (
<svg
width="100%"
height="100%"
viewBox="0 0 360 128"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient
id={`${gradientId}-1`}
x1="38"
y1="41.9469"
x2="16.381"
y2="64.906"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={PRIMARY_DARKER} />
<stop offset="1" stopColor={PRIMARY_MAIN} />
</linearGradient>
<linearGradient
id={`${gradientId}-2`}
x1="21.5"
y1="32"
x2="21.5"
y2="96"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={PRIMARY_LIGHT} />
<stop offset="1" stopColor={PRIMARY_MAIN} />
</linearGradient>
<linearGradient
id={`${gradientId}-3`}
x1="100.5"
y1="72"
x2="100.5"
y2="96"
gradientUnits="userSpaceOnUse"
>
<stop stopColor={PRIMARY_LIGHT} />
<stop offset="1" stopColor={PRIMARY_MAIN} />
</linearGradient>
</defs>
<path
fill={`url(#${`${gradientId}-1`})`}
d="M21.588 61.59C34.378 53.546 40.458 61.254 45.792 71.393C41.379 79.429 38.459 84.333 37.032 86.105C34.343 89.447 31.409 91.978 27.8 93.438C20.214 97.004 10.783 97.17 3.5 92.762L21.588 61.59Z"
/>
<path
fill={`url(#${`${gradientId}-2`})`}
fillRule="evenodd"
clipRule="evenodd"
d="M111.078 57.431C99.51 37.194 87.552 18.124 73.817 46.096C71.937 49.69 70.568 56.68 67.5 56.68V56.644C64.432 56.644 63.063 49.656 61.184 46.06C47.448 18.09 35.49 37.157 23.922 57.396C23.052 58.922 22.216 60.379 21.5 61.645C48.01 44.863 45.778 95.562 67.5 95.965V96C89.223 95.597 86.99 44.899 113.5 61.68C112.785 60.414 111.949 58.957 111.078 57.431Z"
/>
<path
fill={`url(#${`${gradientId}-3`})`}
fillRule="evenodd"
clipRule="evenodd"
d="M112.5 96C119.127 96 124.5 90.627 124.5 84C124.5 77.373 119.127 72 112.5 72C105.873 72 100.5 77.373 100.5 84C100.5 90.627 105.873 96 112.5 96Z"
/>
<path
fill={TEXT_PRIMARY}
fillRule="evenodd"
clipRule="evenodd"
d="M146.031 45.215C149.553 45.215 152.103 42.825 152.103 39.587C152.103 36.348 149.553 34 146.031 34C142.591 34 140 36.348 140 39.587C140 42.826 142.591 45.215 146.031 45.215ZM146.031 93.838C149.351 93.838 151.374 91.854 151.374 87.968V55.984C151.374 52.097 149.351 50.073 146.072 50.073C142.753 50.073 140.729 52.097 140.729 55.983V87.968C140.729 91.814 142.753 93.838 146.031 93.838ZM200.394 88.008C200.394 91.773 198.491 93.838 195.091 93.838C191.65 93.838 189.748 91.733 189.748 87.968V67.563C189.748 61.935 186.955 58.777 182.017 58.777C176.471 58.777 172.99 62.867 172.99 69.547V87.967C172.99 91.733 171.047 93.838 167.647 93.838C164.247 93.838 162.304 91.733 162.304 87.968V55.78C162.304 52.258 164.328 50.072 167.566 50.072C170.764 50.072 172.626 51.975 172.747 55.416V58.048H173.273C174.933 52.946 179.75 49.788 186.064 49.788C195.213 49.788 200.394 55.214 200.394 64.647V88.008ZM216.302 45.215C219.823 45.215 222.374 42.825 222.374 39.587C222.374 36.348 219.823 34 216.302 34C212.861 34 210.27 36.348 210.27 39.587C210.27 42.826 212.861 45.215 216.302 45.215ZM221.645 87.968C221.645 91.854 219.621 93.838 216.302 93.838C213.023 93.838 210.999 91.814 210.999 87.968V55.984C210.999 52.097 213.023 50.073 216.342 50.073C219.621 50.073 221.645 52.097 221.645 55.983V87.968ZM289.001 93.838C292.401 93.838 294.344 91.773 294.344 87.968V63.433C294.344 54.931 289.163 49.789 280.5 49.789C274.307 49.789 269.45 52.907 267.588 57.887H267.102C265.685 52.867 261.314 49.789 255.282 49.789C249.454 49.789 244.96 52.785 243.544 57.603H243.017V55.58C242.856 52.139 240.953 50.074 237.836 50.074C234.598 50.074 232.574 52.26 232.574 55.823V87.969C232.574 91.774 234.517 93.839 237.917 93.839C241.317 93.839 243.26 91.774 243.26 87.969V67.199C243.26 61.977 246.296 58.616 250.83 58.616C255.444 58.616 258.318 61.734 258.318 66.835V87.969C258.318 91.774 260.14 93.839 263.459 93.839C266.819 93.839 268.64 91.774 268.64 87.969V67.239C268.64 62.058 271.757 58.616 276.331 58.616C280.946 58.616 283.698 61.531 283.698 66.633V87.969C283.698 91.774 285.601 93.838 289.001 93.838ZM328.265 87.968C326.079 91.814 321.829 94 316.567 94C308.188 94 302.521 88.737 302.521 80.923C302.521 72.988 308.39 68.089 318.145 68.089H328.346V64.567C328.346 60.235 325.634 57.967 320.736 57.967C317.498 57.967 315.19 59.182 312.802 61.327C311.588 62.3 310.454 62.745 308.714 62.745C306.366 62.745 304.909 61.166 304.909 58.98C304.909 56.47 306.65 53.919 310.414 52.016C313.126 50.599 316.729 49.789 321.424 49.789C332.677 49.789 338.992 55.134 338.992 64.648V88.13C338.992 91.773 337.13 93.838 333.811 93.838C330.977 93.838 329.115 92.259 328.791 89.466V87.968H328.265ZM320.129 86.308C315.96 86.308 313.126 83.838 313.126 80.275C313.126 76.753 315.717 74.648 320.088 74.648H328.346V79.1C328.346 83.149 324.743 86.308 320.129 86.308ZM360 87.968C360 91.854 357.936 93.838 354.657 93.838C351.338 93.838 349.314 91.854 349.314 87.968V40.842C349.314 36.956 351.338 34.932 354.657 34.932C357.936 34.932 360 36.955 360 40.842V87.968Z"
/>
</svg>
);
return (
<LogoRoot
component={RouterLink}
href={href}
aria-label="Logo"
underline="none"
className={mergeClasses([logoClasses.root, className])}
sx={[
{
width: 40,
height: 40,
...(!isSingle && { width: 102, height: 36 }),
...(disabled && { pointerEvents: 'none' }),
},
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
{isSingle ? singleLogo : fullLogo}
</LogoRoot>
);
}
// ----------------------------------------------------------------------
const LogoRoot = styled(Link)(() => ({
flexShrink: 0,
color: 'transparent',
display: 'inline-flex',
verticalAlign: 'middle',
}));

View File

@@ -0,0 +1,7 @@
import { createClasses } from 'src/theme/create-classes';
// ----------------------------------------------------------------------
export const scrollbarClasses = {
root: createClasses('scrollbar__root'),
};

View File

@@ -0,0 +1,5 @@
export * from './classes';
export * from './scrollbar';
export type * from './types';

View File

@@ -0,0 +1,60 @@
import SimpleBar from 'simplebar-react';
import { mergeClasses } from 'minimal-shared/utils';
import { styled } from '@mui/material/styles';
import { scrollbarClasses } from './classes';
import type { ScrollbarProps } from './types';
// ----------------------------------------------------------------------
export function Scrollbar({
sx,
ref,
children,
className,
slotProps,
fillContent = true,
...other
}: ScrollbarProps) {
return (
<ScrollbarRoot
scrollableNodeProps={{ ref }}
clickOnTrack={false}
fillContent={fillContent}
className={mergeClasses([scrollbarClasses.root, className])}
sx={[
{
'& .simplebar-wrapper': slotProps?.wrapperSx as React.CSSProperties,
'& .simplebar-content-wrapper': slotProps?.contentWrapperSx as React.CSSProperties,
'& .simplebar-content': slotProps?.contentSx as React.CSSProperties,
},
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
{children}
</ScrollbarRoot>
);
}
// ----------------------------------------------------------------------
const ScrollbarRoot = styled(SimpleBar, {
shouldForwardProp: (prop: string) => !['fillContent', 'sx'].includes(prop),
})<Pick<ScrollbarProps, 'fillContent'>>(({ fillContent }) => ({
minWidth: 0,
minHeight: 0,
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
...(fillContent && {
'& .simplebar-content': {
display: 'flex',
flex: '1 1 auto',
minHeight: '100%',
flexDirection: 'column',
},
}),
}));

View File

@@ -0,0 +1,8 @@
@import 'simplebar-react/dist/simplebar.min.css';
.simplebar-scrollbar:before {
background-color: var(--palette-text-disabled);
}
.simplebar-scrollbar.simplebar-visible:before {
opacity: 0.48;
}

View File

@@ -0,0 +1,15 @@
import type { Theme, SxProps } from '@mui/material/styles';
import type { Props as SimplebarProps } from 'simplebar-react';
// ----------------------------------------------------------------------
export type ScrollbarProps = SimplebarProps &
React.ComponentProps<'div'> & {
sx?: SxProps<Theme>;
fillContent?: boolean;
slotProps?: {
wrapperSx?: SxProps<Theme>;
contentSx?: SxProps<Theme>;
contentWrapperSx?: SxProps<Theme>;
};
};

View File

@@ -0,0 +1,7 @@
import { createClasses } from 'src/theme/create-classes';
// ----------------------------------------------------------------------
export const svgColorClasses = {
root: createClasses('svg__color__root'),
};

View File

@@ -0,0 +1,5 @@
export * from './classes';
export * from './svg-color';
export type * from './types';

View File

@@ -0,0 +1,35 @@
import { mergeClasses } from 'minimal-shared/utils';
import { styled } from '@mui/material/styles';
import { svgColorClasses } from './classes';
import type { SvgColorProps } from './types';
// ----------------------------------------------------------------------
export function SvgColor({ src, className, sx, ...other }: SvgColorProps) {
return (
<SvgRoot
className={mergeClasses([svgColorClasses.root, className])}
sx={[
{
mask: `url(${src}) no-repeat center / contain`,
WebkitMask: `url(${src}) no-repeat center / contain`,
},
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
/>
);
}
// ----------------------------------------------------------------------
const SvgRoot = styled('span')(() => ({
width: 24,
height: 24,
flexShrink: 0,
display: 'inline-flex',
backgroundColor: 'currentColor',
}));

View File

@@ -0,0 +1,8 @@
import type { Theme, SxProps } from '@mui/material/styles';
// ----------------------------------------------------------------------
export type SvgColorProps = React.ComponentProps<'span'> & {
src: string;
sx?: SxProps<Theme>;
};

View File

@@ -0,0 +1,13 @@
import packageJson from '../package.json';
// ----------------------------------------------------------------------
export type ConfigValue = {
appName: string;
appVersion: string;
};
export const CONFIG: ConfigValue = {
appName: 'Minimal UI',
appVersion: packageJson.version,
};

View File

@@ -0,0 +1,56 @@
/** **************************************
* Fonts: app
*************************************** */
@import '@fontsource-variable/dm-sans';
@import '@fontsource/barlow/400.css';
@import '@fontsource/barlow/500.css';
@import '@fontsource/barlow/600.css';
@import '@fontsource/barlow/700.css';
@import '@fontsource/barlow/800.css';
/** **************************************
* Plugins
*************************************** */
/* scrollbar */
@import './components/scrollbar/styles.css';
/* chart */
@import './components/chart/styles.css';
/** **************************************
* Baseline
*************************************** */
html {
height: 100%;
-webkit-overflow-scrolling: touch;
}
body,
#root,
#root__layout {
display: flex;
flex: 1 1 auto;
min-height: 100%;
flex-direction: column;
}
img {
max-width: 100%;
vertical-align: middle;
}
ul {
margin: 0;
padding: 0;
list-style-type: none;
}
input[type='number'] {
-moz-appearance: textfield;
appearance: none;
}
input[type='number']::-webkit-outer-spin-button {
margin: 0;
-webkit-appearance: none;
}
input[type='number']::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
}

View File

@@ -0,0 +1,36 @@
import type { BoxProps } from '@mui/material/Box';
import { mergeClasses } from 'minimal-shared/utils';
import Box from '@mui/material/Box';
import { layoutClasses } from '../core/classes';
// ----------------------------------------------------------------------
export type AuthContentProps = BoxProps;
export function AuthContent({ sx, children, className, ...other }: AuthContentProps) {
return (
<Box
className={mergeClasses([layoutClasses.content, className])}
sx={[
(theme) => ({
py: 5,
px: 3,
width: 1,
zIndex: 2,
borderRadius: 2,
display: 'flex',
flexDirection: 'column',
maxWidth: 'var(--layout-auth-content-width)',
bgcolor: theme.vars.palette.background.default,
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
{children}
</Box>
);
}

View File

@@ -0,0 +1,3 @@
export * from './layout';
export * from './content';

View File

@@ -0,0 +1,148 @@
import type { CSSObject, Breakpoint } from '@mui/material/styles';
import { merge } from 'es-toolkit';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Alert from '@mui/material/Alert';
import { RouterLink } from 'src/routes/components';
import { Logo } from 'src/components/logo';
import { AuthContent } from './content';
import { MainSection } from '../core/main-section';
import { LayoutSection } from '../core/layout-section';
import { HeaderSection } from '../core/header-section';
import type { AuthContentProps } from './content';
import type { MainSectionProps } from '../core/main-section';
import type { HeaderSectionProps } from '../core/header-section';
import type { LayoutSectionProps } from '../core/layout-section';
// ----------------------------------------------------------------------
type LayoutBaseProps = Pick<LayoutSectionProps, 'sx' | 'children' | 'cssVars'>;
export type AuthLayoutProps = LayoutBaseProps & {
layoutQuery?: Breakpoint;
slotProps?: {
header?: HeaderSectionProps;
main?: MainSectionProps;
content?: AuthContentProps;
};
};
export function AuthLayout({
sx,
cssVars,
children,
slotProps,
layoutQuery = 'md',
}: AuthLayoutProps) {
const renderHeader = () => {
const headerSlotProps: HeaderSectionProps['slotProps'] = { container: { maxWidth: false } };
const headerSlots: HeaderSectionProps['slots'] = {
topArea: (
<Alert severity="info" sx={{ display: 'none', borderRadius: 0 }}>
This is an info Alert.
</Alert>
),
leftArea: (
<>
{/** @slot Logo */}
<Logo />
</>
),
rightArea: (
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 1.5 } }}>
{/** @slot Help link */}
<Link href="#" component={RouterLink} color="inherit" sx={{ typography: 'subtitle2' }}>
Need help?
</Link>
</Box>
),
};
return (
<HeaderSection
disableElevation
layoutQuery={layoutQuery}
{...slotProps?.header}
slots={{ ...headerSlots, ...slotProps?.header?.slots }}
slotProps={merge(headerSlotProps, slotProps?.header?.slotProps ?? {})}
sx={[
{ position: { [layoutQuery]: 'fixed' } },
...(Array.isArray(slotProps?.header?.sx)
? (slotProps?.header?.sx ?? [])
: [slotProps?.header?.sx]),
]}
/>
);
};
const renderFooter = () => null;
const renderMain = () => (
<MainSection
{...slotProps?.main}
sx={[
(theme) => ({
alignItems: 'center',
p: theme.spacing(3, 2, 10, 2),
[theme.breakpoints.up(layoutQuery)]: {
justifyContent: 'center',
p: theme.spacing(10, 0, 10, 0),
},
}),
...(Array.isArray(slotProps?.main?.sx)
? (slotProps?.main?.sx ?? [])
: [slotProps?.main?.sx]),
]}
>
<AuthContent {...slotProps?.content}>{children}</AuthContent>
</MainSection>
);
return (
<LayoutSection
/** **************************************
* @Header
*************************************** */
headerSection={renderHeader()}
/** **************************************
* @Footer
*************************************** */
footerSection={renderFooter()}
/** **************************************
* @Styles
*************************************** */
cssVars={{ '--layout-auth-content-width': '420px', ...cssVars }}
sx={[
(theme) => ({
position: 'relative',
'&::before': backgroundStyles(),
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
>
{renderMain()}
</LayoutSection>
);
}
// ----------------------------------------------------------------------
const backgroundStyles = (): CSSObject => ({
zIndex: 1,
opacity: 0.24,
width: '100%',
height: '100%',
content: "''",
position: 'absolute',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center center',
backgroundImage: 'url(/assets/background/overlay.jpg)',
});

View File

@@ -0,0 +1,139 @@
import type { IconButtonProps } from '@mui/material/IconButton';
import { useState, useCallback } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Avatar from '@mui/material/Avatar';
import Popover from '@mui/material/Popover';
import Divider from '@mui/material/Divider';
import MenuList from '@mui/material/MenuList';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import MenuItem, { menuItemClasses } from '@mui/material/MenuItem';
import { useRouter, usePathname } from 'src/routes/hooks';
import { _myAccount } from 'src/_mock';
// ----------------------------------------------------------------------
export type AccountPopoverProps = IconButtonProps & {
data?: {
label: string;
href: string;
icon?: React.ReactNode;
info?: React.ReactNode;
}[];
};
export function AccountPopover({ data = [], sx, ...other }: AccountPopoverProps) {
const router = useRouter();
const pathname = usePathname();
const [openPopover, setOpenPopover] = useState<HTMLButtonElement | null>(null);
const handleOpenPopover = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
setOpenPopover(event.currentTarget);
}, []);
const handleClosePopover = useCallback(() => {
setOpenPopover(null);
}, []);
const handleClickItem = useCallback(
(path: string) => {
handleClosePopover();
router.push(path);
},
[handleClosePopover, router]
);
return (
<>
<IconButton
onClick={handleOpenPopover}
sx={{
p: '2px',
width: 40,
height: 40,
background: (theme) =>
`conic-gradient(${theme.vars.palette.primary.light}, ${theme.vars.palette.warning.light}, ${theme.vars.palette.primary.light})`,
...sx,
}}
{...other}
>
<Avatar src={_myAccount.photoURL} alt={_myAccount.displayName} sx={{ width: 1, height: 1 }}>
{_myAccount.displayName.charAt(0).toUpperCase()}
</Avatar>
</IconButton>
<Popover
open={!!openPopover}
anchorEl={openPopover}
onClose={handleClosePopover}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
slotProps={{
paper: {
sx: { width: 200 },
},
}}
>
<Box sx={{ p: 2, pb: 1.5 }}>
<Typography variant="subtitle2" noWrap>
{_myAccount?.displayName}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }} noWrap>
{_myAccount?.email}
</Typography>
</Box>
<Divider sx={{ borderStyle: 'dashed' }} />
<MenuList
disablePadding
sx={{
p: 1,
gap: 0.5,
display: 'flex',
flexDirection: 'column',
[`& .${menuItemClasses.root}`]: {
px: 1,
gap: 2,
borderRadius: 0.75,
color: 'text.secondary',
'&:hover': { color: 'text.primary' },
[`&.${menuItemClasses.selected}`]: {
color: 'text.primary',
bgcolor: 'action.selected',
fontWeight: 'fontWeightSemiBold',
},
},
}}
>
{data.map((option) => (
<MenuItem
key={option.label}
selected={option.href === pathname}
onClick={() => handleClickItem(option.href)}
>
{option.icon}
{option.label}
</MenuItem>
))}
</MenuList>
<Divider sx={{ borderStyle: 'dashed' }} />
<Box sx={{ p: 1 }}>
<Button fullWidth color="error" size="medium" variant="text">
Logout
</Button>
</Box>
</Popover>
</>
);
}

View File

@@ -0,0 +1,109 @@
import type { IconButtonProps } from '@mui/material/IconButton';
import { useState, useCallback } from 'react';
import { usePopover } from 'minimal-shared/hooks';
import Box from '@mui/material/Box';
import Popover from '@mui/material/Popover';
import MenuList from '@mui/material/MenuList';
import IconButton from '@mui/material/IconButton';
import MenuItem, { menuItemClasses } from '@mui/material/MenuItem';
// ----------------------------------------------------------------------
export type LanguagePopoverProps = IconButtonProps & {
data?: {
value: string;
label: string;
icon: string;
}[];
};
export function LanguagePopover({ data = [], sx, ...other }: LanguagePopoverProps) {
const { open, anchorEl, onClose, onOpen } = usePopover();
const [locale, setLocale] = useState(data[0].value);
const handleChangeLang = useCallback(
(newLang: string) => {
setLocale(newLang);
onClose();
},
[onClose]
);
const currentLang = data.find((lang) => lang.value === locale);
const renderFlag = (label?: string, icon?: string) => (
<Box
component="img"
alt={label}
src={icon}
sx={{ width: 26, height: 20, borderRadius: 0.5, objectFit: 'cover' }}
/>
);
const renderMenuList = () => (
<Popover
open={open}
anchorEl={anchorEl}
onClose={onClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<MenuList
sx={{
p: 0.5,
gap: 0.5,
width: 160,
minHeight: 72,
display: 'flex',
flexDirection: 'column',
[`& .${menuItemClasses.root}`]: {
px: 1,
gap: 2,
borderRadius: 0.75,
[`&.${menuItemClasses.selected}`]: {
bgcolor: 'action.selected',
fontWeight: 'fontWeightSemiBold',
},
},
}}
>
{data?.map((option) => (
<MenuItem
key={option.value}
selected={option.value === currentLang?.value}
onClick={() => handleChangeLang(option.value)}
>
{renderFlag(option.label, option.icon)}
{option.label}
</MenuItem>
))}
</MenuList>
</Popover>
);
return (
<>
<IconButton
aria-label="Languages button"
onClick={onOpen}
sx={[
(theme) => ({
p: 0,
width: 40,
height: 40,
...(open && { bgcolor: theme.vars.palette.action.selected }),
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
{renderFlag(currentLang?.label, currentLang?.icon)}
</IconButton>
{renderMenuList()}
</>
);
}

View File

@@ -0,0 +1,15 @@
import type { IconButtonProps } from '@mui/material/IconButton';
import IconButton from '@mui/material/IconButton';
import { Iconify } from 'src/components/iconify';
// ----------------------------------------------------------------------
export function MenuButton({ sx, ...other }: IconButtonProps) {
return (
<IconButton sx={sx} {...other}>
<Iconify icon="custom:menu-duotone" width={24} />
</IconButton>
);
}

View File

@@ -0,0 +1,64 @@
import type { StackProps } from '@mui/material/Stack';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
// ----------------------------------------------------------------------
export function NavUpgrade({ sx, ...other }: StackProps) {
return (
<Box
sx={[
{
mb: 4,
display: 'flex',
textAlign: 'center',
alignItems: 'center',
flexDirection: 'column',
},
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
<Typography
variant="h6"
sx={[
(theme) => ({
background: `linear-gradient(to right, ${theme.vars.palette.secondary.main}, ${theme.vars.palette.warning.main})`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
textFillColor: 'transparent',
color: 'transparent',
}),
]}
>
More features?
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary', mt: 0.5 }}>
{`From only `}
<Box component="strong" sx={{ color: 'text.primary' }}>
$69
</Box>
</Typography>
<Box
component="img"
alt="Minimal dashboard"
src="/assets/illustrations/illustration-dashboard.webp"
sx={{ width: 200, my: 2 }}
/>
<Button
href="https://material-ui.com/store/items/minimal-dashboard/"
target="_blank"
variant="contained"
color="inherit"
>
Upgrade to Pro
</Button>
</Box>
);
}

View File

@@ -0,0 +1,259 @@
import type { IconButtonProps } from '@mui/material/IconButton';
import { useState, useCallback } from 'react';
import Box from '@mui/material/Box';
import List from '@mui/material/List';
import Badge from '@mui/material/Badge';
import Button from '@mui/material/Button';
import Avatar from '@mui/material/Avatar';
import Divider from '@mui/material/Divider';
import Tooltip from '@mui/material/Tooltip';
import Popover from '@mui/material/Popover';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import ListItemButton from '@mui/material/ListItemButton';
import { fToNow } from 'src/utils/format-time';
import { Iconify } from 'src/components/iconify';
import { Scrollbar } from 'src/components/scrollbar';
// ----------------------------------------------------------------------
type NotificationItemProps = {
id: string;
type: string;
title: string;
isUnRead: boolean;
description: string;
avatarUrl: string | null;
postedAt: string | number | null;
};
export type NotificationsPopoverProps = IconButtonProps & {
data?: NotificationItemProps[];
};
export function NotificationsPopover({ data = [], sx, ...other }: NotificationsPopoverProps) {
const [notifications, setNotifications] = useState(data);
const totalUnRead = notifications.filter((item) => item.isUnRead === true).length;
const [openPopover, setOpenPopover] = useState<HTMLButtonElement | null>(null);
const handleOpenPopover = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
setOpenPopover(event.currentTarget);
}, []);
const handleClosePopover = useCallback(() => {
setOpenPopover(null);
}, []);
const handleMarkAllAsRead = useCallback(() => {
const updatedNotifications = notifications.map((notification) => ({
...notification,
isUnRead: false,
}));
setNotifications(updatedNotifications);
}, [notifications]);
return (
<>
<IconButton
color={openPopover ? 'primary' : 'default'}
onClick={handleOpenPopover}
sx={sx}
{...other}
>
<Badge badgeContent={totalUnRead} color="error">
<Iconify width={24} icon="solar:bell-bing-bold-duotone" />
</Badge>
</IconButton>
<Popover
open={!!openPopover}
anchorEl={openPopover}
onClose={handleClosePopover}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
slotProps={{
paper: {
sx: {
width: 360,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
},
},
}}
>
<Box
sx={{
py: 2,
pl: 2.5,
pr: 1.5,
display: 'flex',
alignItems: 'center',
}}
>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle1">Notifications</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
You have {totalUnRead} unread messages
</Typography>
</Box>
{totalUnRead > 0 && (
<Tooltip title=" Mark all as read">
<IconButton color="primary" onClick={handleMarkAllAsRead}>
<Iconify icon="eva:done-all-fill" />
</IconButton>
</Tooltip>
)}
</Box>
<Divider sx={{ borderStyle: 'dashed' }} />
<Scrollbar fillContent sx={{ minHeight: 240, maxHeight: { xs: 360, sm: 'none' } }}>
<List
disablePadding
subheader={
<ListSubheader disableSticky sx={{ py: 1, px: 2.5, typography: 'overline' }}>
New
</ListSubheader>
}
>
{notifications.slice(0, 2).map((notification) => (
<NotificationItem key={notification.id} notification={notification} />
))}
</List>
<List
disablePadding
subheader={
<ListSubheader disableSticky sx={{ py: 1, px: 2.5, typography: 'overline' }}>
Before that
</ListSubheader>
}
>
{notifications.slice(2, 5).map((notification) => (
<NotificationItem key={notification.id} notification={notification} />
))}
</List>
</Scrollbar>
<Divider sx={{ borderStyle: 'dashed' }} />
<Box sx={{ p: 1 }}>
<Button fullWidth disableRipple color="inherit">
View all
</Button>
</Box>
</Popover>
</>
);
}
// ----------------------------------------------------------------------
function NotificationItem({ notification }: { notification: NotificationItemProps }) {
const { avatarUrl, title } = renderContent(notification);
return (
<ListItemButton
sx={{
py: 1.5,
px: 2.5,
mt: '1px',
...(notification.isUnRead && {
bgcolor: 'action.selected',
}),
}}
>
<ListItemAvatar>
<Avatar sx={{ bgcolor: 'background.neutral' }}>{avatarUrl}</Avatar>
</ListItemAvatar>
<ListItemText
primary={title}
secondary={
<Typography
variant="caption"
sx={{
mt: 0.5,
gap: 0.5,
display: 'flex',
alignItems: 'center',
color: 'text.disabled',
}}
>
<Iconify width={14} icon="solar:clock-circle-outline" />
{fToNow(notification.postedAt)}
</Typography>
}
/>
</ListItemButton>
);
}
// ----------------------------------------------------------------------
function renderContent(notification: NotificationItemProps) {
const title = (
<Typography variant="subtitle2">
{notification.title}
<Typography component="span" variant="body2" sx={{ color: 'text.secondary' }}>
&nbsp; {notification.description}
</Typography>
</Typography>
);
if (notification.type === 'order-placed') {
return {
avatarUrl: (
<img
alt={notification.title}
src="/assets/icons/notification/ic-notification-package.svg"
/>
),
title,
};
}
if (notification.type === 'order-shipped') {
return {
avatarUrl: (
<img
alt={notification.title}
src="/assets/icons/notification/ic-notification-shipping.svg"
/>
),
title,
};
}
if (notification.type === 'mail') {
return {
avatarUrl: (
<img alt={notification.title} src="/assets/icons/notification/ic-notification-mail.svg" />
),
title,
};
}
if (notification.type === 'chat-message') {
return {
avatarUrl: (
<img alt={notification.title} src="/assets/icons/notification/ic-notification-chat.svg" />
),
title,
};
}
return {
avatarUrl: notification.avatarUrl ? (
<img alt={notification.title} src={notification.avatarUrl} />
) : null,
title,
};
}

View File

@@ -0,0 +1,84 @@
import type { BoxProps } from '@mui/material/Box';
import { useState, useCallback } from 'react';
import { varAlpha } from 'minimal-shared/utils';
import Box from '@mui/material/Box';
import Slide from '@mui/material/Slide';
import Input from '@mui/material/Input';
import Button from '@mui/material/Button';
import { useTheme } from '@mui/material/styles';
import IconButton from '@mui/material/IconButton';
import InputAdornment from '@mui/material/InputAdornment';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import { Iconify } from 'src/components/iconify';
// ----------------------------------------------------------------------
export function Searchbar({ sx, ...other }: BoxProps) {
const theme = useTheme();
const [open, setOpen] = useState(false);
const handleOpen = useCallback(() => {
setOpen((prev) => !prev);
}, []);
const handleClose = useCallback(() => {
setOpen(false);
}, []);
return (
<ClickAwayListener onClickAway={handleClose}>
<div>
{!open && (
<IconButton onClick={handleOpen}>
<Iconify icon="eva:search-fill" />
</IconButton>
)}
<Slide direction="down" in={open} mountOnEnter unmountOnExit>
<Box
sx={{
top: 0,
left: 0,
zIndex: 99,
width: '100%',
display: 'flex',
position: 'absolute',
alignItems: 'center',
px: { xs: 3, md: 5 },
boxShadow: theme.vars.customShadows.z8,
height: {
xs: 'var(--layout-header-mobile-height)',
md: 'var(--layout-header-desktop-height)',
},
backdropFilter: `blur(6px)`,
WebkitBackdropFilter: `blur(6px)`,
backgroundColor: varAlpha(theme.vars.palette.background.defaultChannel, 0.8),
...sx,
}}
{...other}
>
<Input
autoFocus
fullWidth
disableUnderline
placeholder="Search…"
startAdornment={
<InputAdornment position="start">
<Iconify width={20} icon="eva:search-fill" sx={{ color: 'text.disabled' }} />
</InputAdornment>
}
sx={{ fontWeight: 'fontWeightBold' }}
/>
<Button variant="contained" onClick={handleClose}>
Search
</Button>
</Box>
</Slide>
</div>
</ClickAwayListener>
);
}

View File

@@ -0,0 +1,132 @@
import type { ButtonBaseProps } from '@mui/material/ButtonBase';
import { useState, useCallback } from 'react';
import { varAlpha } from 'minimal-shared/utils';
import Box from '@mui/material/Box';
import Popover from '@mui/material/Popover';
import MenuList from '@mui/material/MenuList';
import ButtonBase from '@mui/material/ButtonBase';
import MenuItem, { menuItemClasses } from '@mui/material/MenuItem';
import { Label } from 'src/components/label';
import { Iconify } from 'src/components/iconify';
// ----------------------------------------------------------------------
export type WorkspacesPopoverProps = ButtonBaseProps & {
data?: {
id: string;
name: string;
logo: string;
plan: string;
}[];
};
export function WorkspacesPopover({ data = [], sx, ...other }: WorkspacesPopoverProps) {
const [workspace, setWorkspace] = useState(data[0]);
const [openPopover, setOpenPopover] = useState<HTMLButtonElement | null>(null);
const handleOpenPopover = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
setOpenPopover(event.currentTarget);
}, []);
const handleClosePopover = useCallback(() => {
setOpenPopover(null);
}, []);
const handleChangeWorkspace = useCallback(
(newValue: (typeof data)[number]) => {
setWorkspace(newValue);
handleClosePopover();
},
[handleClosePopover]
);
const renderAvatar = (alt: string, src: string) => (
<Box component="img" alt={alt} src={src} sx={{ width: 24, height: 24, borderRadius: '50%' }} />
);
const renderLabel = (plan: string) => (
<Label color={plan === 'Free' ? 'default' : 'info'}>{plan}</Label>
);
return (
<>
<ButtonBase
disableRipple
onClick={handleOpenPopover}
sx={{
pl: 2,
py: 3,
gap: 1.5,
pr: 1.5,
width: 1,
borderRadius: 1.5,
textAlign: 'left',
justifyContent: 'flex-start',
bgcolor: (theme) => varAlpha(theme.vars.palette.grey['500Channel'], 0.08),
...sx,
}}
{...other}
>
{renderAvatar(workspace?.name, workspace?.logo)}
<Box
sx={{
gap: 1,
flexGrow: 1,
display: 'flex',
alignItems: 'center',
typography: 'body2',
fontWeight: 'fontWeightSemiBold',
}}
>
{workspace?.name}
{renderLabel(workspace?.plan)}
</Box>
<Iconify width={16} icon="carbon:chevron-sort" sx={{ color: 'text.disabled' }} />
</ButtonBase>
<Popover open={!!openPopover} anchorEl={openPopover} onClose={handleClosePopover}>
<MenuList
disablePadding
sx={{
p: 0.5,
gap: 0.5,
width: 260,
display: 'flex',
flexDirection: 'column',
[`& .${menuItemClasses.root}`]: {
p: 1.5,
gap: 1.5,
borderRadius: 0.75,
[`&.${menuItemClasses.selected}`]: {
bgcolor: 'action.selected',
fontWeight: 'fontWeightSemiBold',
},
},
}}
>
{data.map((option) => (
<MenuItem
key={option.id}
selected={option.id === workspace?.id}
onClick={() => handleChangeWorkspace(option)}
>
{renderAvatar(option.name, option.logo)}
<Box component="span" sx={{ flexGrow: 1 }}>
{option.name}
</Box>
{renderLabel(option.plan)}
</MenuItem>
))}
</MenuList>
</Popover>
</>
);
}

View File

@@ -0,0 +1,17 @@
import { createClasses } from 'src/theme/create-classes';
// ----------------------------------------------------------------------
export const layoutClasses = {
root: createClasses('layout__root'),
main: createClasses('layout__main'),
header: createClasses('layout__header'),
nav: {
root: createClasses('layout__nav__root'),
mobile: createClasses('layout__nav__mobile'),
vertical: createClasses('layout__nav__vertical'),
horizontal: createClasses('layout__nav__horizontal'),
},
content: createClasses('layout__main__content'),
sidebarContainer: createClasses('layout__sidebar__container'),
};

View File

@@ -0,0 +1,14 @@
import type { Theme } from '@mui/material/styles';
// ----------------------------------------------------------------------
export function layoutSectionVars(theme: Theme) {
return {
'--layout-nav-zIndex': theme.zIndex.drawer + 1,
'--layout-nav-mobile-width': '288px',
'--layout-header-blur': '8px',
'--layout-header-zIndex': theme.zIndex.appBar + 1,
'--layout-header-mobile-height': '64px',
'--layout-header-desktop-height': '72px',
};
}

View File

@@ -0,0 +1,153 @@
import type { AppBarProps } from '@mui/material/AppBar';
import type { ContainerProps } from '@mui/material/Container';
import type { Theme, SxProps, CSSObject, Breakpoint } from '@mui/material/styles';
import { useScrollOffsetTop } from 'minimal-shared/hooks';
import { varAlpha, mergeClasses } from 'minimal-shared/utils';
import AppBar from '@mui/material/AppBar';
import { styled } from '@mui/material/styles';
import Container from '@mui/material/Container';
import { layoutClasses } from './classes';
// ----------------------------------------------------------------------
export type HeaderSectionProps = AppBarProps & {
layoutQuery?: Breakpoint;
disableOffset?: boolean;
disableElevation?: boolean;
slots?: {
leftArea?: React.ReactNode;
rightArea?: React.ReactNode;
topArea?: React.ReactNode;
centerArea?: React.ReactNode;
bottomArea?: React.ReactNode;
};
slotProps?: {
container?: ContainerProps;
centerArea?: React.ComponentProps<'div'> & { sx?: SxProps<Theme> };
};
};
export function HeaderSection({
sx,
slots,
slotProps,
className,
disableOffset,
disableElevation,
layoutQuery = 'md',
...other
}: HeaderSectionProps) {
const { offsetTop: isOffset } = useScrollOffsetTop();
return (
<HeaderRoot
position="sticky"
color="transparent"
isOffset={isOffset}
disableOffset={disableOffset}
disableElevation={disableElevation}
className={mergeClasses([layoutClasses.header, className])}
sx={[
(theme) => ({
...(isOffset && {
'--color': `var(--offset-color, ${theme.vars.palette.text.primary})`,
}),
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
{slots?.topArea}
<HeaderContainer layoutQuery={layoutQuery} {...slotProps?.container}>
{slots?.leftArea}
<HeaderCenterArea {...slotProps?.centerArea}>{slots?.centerArea}</HeaderCenterArea>
{slots?.rightArea}
</HeaderContainer>
{slots?.bottomArea}
</HeaderRoot>
);
}
// ----------------------------------------------------------------------
type HeaderRootProps = Pick<HeaderSectionProps, 'disableOffset' | 'disableElevation'> & {
isOffset: boolean;
};
const HeaderRoot = styled(AppBar, {
shouldForwardProp: (prop: string) =>
!['isOffset', 'disableOffset', 'disableElevation', 'sx'].includes(prop),
})<HeaderRootProps>(({ isOffset, disableOffset, disableElevation, theme }) => {
const pauseZindex = { top: -1, bottom: -2 };
const pauseStyles: CSSObject = {
opacity: 0,
content: '""',
visibility: 'hidden',
position: 'absolute',
transition: theme.transitions.create(['opacity', 'visibility'], {
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.shorter,
}),
};
const bgStyles: CSSObject = {
...pauseStyles,
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: pauseZindex.top,
backdropFilter: `blur(6px)`,
WebkitBackdropFilter: `blur(6px)`,
backgroundColor: varAlpha(theme.vars.palette.background.defaultChannel, 0.8),
...(isOffset && {
opacity: 1,
visibility: 'visible',
}),
};
const shadowStyles: CSSObject = {
...pauseStyles,
left: 0,
right: 0,
bottom: 0,
height: 24,
margin: 'auto',
borderRadius: '50%',
width: `calc(100% - 48px)`,
zIndex: pauseZindex.bottom,
boxShadow: theme.vars.customShadows.z8,
...(isOffset && { opacity: 0.48, visibility: 'visible' }),
};
return {
boxShadow: 'none',
zIndex: 'var(--layout-header-zIndex)',
...(!disableOffset && { '&::before': bgStyles }),
...(!disableElevation && { '&::after': shadowStyles }),
};
});
const HeaderContainer = styled(Container, {
shouldForwardProp: (prop: string) => !['layoutQuery', 'sx'].includes(prop),
})<Pick<HeaderSectionProps, 'layoutQuery'>>(({ layoutQuery = 'md', theme }) => ({
display: 'flex',
alignItems: 'center',
color: 'var(--color)',
height: 'var(--layout-header-mobile-height)',
[theme.breakpoints.up(layoutQuery)]: { height: 'var(--layout-header-desktop-height)' },
}));
const HeaderCenterArea = styled('div')(() => ({
display: 'flex',
flex: '1 1 auto',
justifyContent: 'center',
}));

View File

@@ -0,0 +1,9 @@
export * from './classes';
export * from './css-vars';
export * from './main-section';
export * from './layout-section';
export * from './header-section';

View File

@@ -0,0 +1,75 @@
import type { Theme, SxProps, CSSObject } from '@mui/material/styles';
import { mergeClasses } from 'minimal-shared/utils';
import { styled } from '@mui/material/styles';
import GlobalStyles from '@mui/material/GlobalStyles';
import { layoutClasses } from './classes';
import { layoutSectionVars } from './css-vars';
// ----------------------------------------------------------------------
export type LayoutSectionProps = React.ComponentProps<'div'> & {
sx?: SxProps<Theme>;
cssVars?: CSSObject;
children?: React.ReactNode;
footerSection?: React.ReactNode;
headerSection?: React.ReactNode;
sidebarSection?: React.ReactNode;
};
export function LayoutSection({
sx,
cssVars,
children,
footerSection,
headerSection,
sidebarSection,
className,
...other
}: LayoutSectionProps) {
const inputGlobalStyles = (
<GlobalStyles styles={(theme) => ({ body: { ...layoutSectionVars(theme), ...cssVars } })} />
);
return (
<>
{inputGlobalStyles}
<LayoutRoot
id="root__layout"
className={mergeClasses([layoutClasses.root, className])}
sx={sx}
{...other}
>
{sidebarSection ? (
<>
{sidebarSection}
<LayoutSidebarContainer className={layoutClasses.sidebarContainer}>
{headerSection}
{children}
{footerSection}
</LayoutSidebarContainer>
</>
) : (
<>
{headerSection}
{children}
{footerSection}
</>
)}
</LayoutRoot>
</>
);
}
// ----------------------------------------------------------------------
const LayoutRoot = styled('div')``;
const LayoutSidebarContainer = styled('div')(() => ({
display: 'flex',
flex: '1 1 auto',
flexDirection: 'column',
}));

View File

@@ -0,0 +1,25 @@
import { mergeClasses } from 'minimal-shared/utils';
import { styled } from '@mui/material/styles';
import { layoutClasses } from './classes';
// ----------------------------------------------------------------------
export type MainSectionProps = React.ComponentProps<typeof MainRoot>;
export function MainSection({ children, className, sx, ...other }: MainSectionProps) {
return (
<MainRoot className={mergeClasses([layoutClasses.main, className])} sx={sx} {...other}>
{children}
</MainRoot>
);
}
// ----------------------------------------------------------------------
const MainRoot = styled('main')({
display: 'flex',
flex: '1 1 auto',
flexDirection: 'column',
});

View File

@@ -0,0 +1,57 @@
import type { Breakpoint } from '@mui/material/styles';
import type { ContainerProps } from '@mui/material/Container';
import { mergeClasses } from 'minimal-shared/utils';
import Container from '@mui/material/Container';
import { layoutClasses } from '../core/classes';
// ----------------------------------------------------------------------
export type DashboardContentProps = ContainerProps & {
layoutQuery?: Breakpoint;
disablePadding?: boolean;
};
export function DashboardContent({
sx,
children,
className,
disablePadding,
maxWidth = 'lg',
layoutQuery = 'lg',
...other
}: DashboardContentProps) {
return (
<Container
className={mergeClasses([layoutClasses.content, className])}
maxWidth={maxWidth}
sx={[
(theme) => ({
display: 'flex',
flex: '1 1 auto',
flexDirection: 'column',
pt: 'var(--layout-dashboard-content-pt)',
pb: 'var(--layout-dashboard-content-pb)',
[theme.breakpoints.up(layoutQuery)]: {
px: 'var(--layout-dashboard-content-px)',
},
...(disablePadding && {
p: {
xs: 0,
sm: 0,
md: 0,
lg: 0,
xl: 0,
},
}),
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
{children}
</Container>
);
}

View File

@@ -0,0 +1,14 @@
import type { Theme } from '@mui/material/styles';
// ----------------------------------------------------------------------
export function dashboardLayoutVars(theme: Theme) {
return {
'--layout-transition-easing': 'linear',
'--layout-transition-duration': '120ms',
'--layout-nav-vertical-width': '300px',
'--layout-dashboard-content-pt': theme.spacing(1),
'--layout-dashboard-content-pb': theme.spacing(8),
'--layout-dashboard-content-px': theme.spacing(5),
};
}

View File

@@ -0,0 +1,3 @@
export * from './layout';
export * from './content';

View File

@@ -0,0 +1,148 @@
import type { Breakpoint } from '@mui/material/styles';
import { merge } from 'es-toolkit';
import { useBoolean } from 'minimal-shared/hooks';
import Box from '@mui/material/Box';
import Alert from '@mui/material/Alert';
import { useTheme } from '@mui/material/styles';
import { _langs, _notifications } from 'src/_mock';
import { NavMobile, NavDesktop } from './nav';
import { layoutClasses } from '../core/classes';
import { _account } from '../nav-config-account';
import { dashboardLayoutVars } from './css-vars';
import { navData } from '../nav-config-dashboard';
import { MainSection } from '../core/main-section';
import { Searchbar } from '../components/searchbar';
import { _workspaces } from '../nav-config-workspace';
import { MenuButton } from '../components/menu-button';
import { HeaderSection } from '../core/header-section';
import { LayoutSection } from '../core/layout-section';
import { AccountPopover } from '../components/account-popover';
import { LanguagePopover } from '../components/language-popover';
import { NotificationsPopover } from '../components/notifications-popover';
import type { MainSectionProps } from '../core/main-section';
import type { HeaderSectionProps } from '../core/header-section';
import type { LayoutSectionProps } from '../core/layout-section';
// ----------------------------------------------------------------------
type LayoutBaseProps = Pick<LayoutSectionProps, 'sx' | 'children' | 'cssVars'>;
export type DashboardLayoutProps = LayoutBaseProps & {
layoutQuery?: Breakpoint;
slotProps?: {
header?: HeaderSectionProps;
main?: MainSectionProps;
};
};
export function DashboardLayout({
sx,
cssVars,
children,
slotProps,
layoutQuery = 'lg',
}: DashboardLayoutProps) {
const theme = useTheme();
const { value: open, onFalse: onClose, onTrue: onOpen } = useBoolean();
const renderHeader = () => {
const headerSlotProps: HeaderSectionProps['slotProps'] = {
container: {
maxWidth: false,
},
};
const headerSlots: HeaderSectionProps['slots'] = {
topArea: (
<Alert severity="info" sx={{ display: 'none', borderRadius: 0 }}>
This is an info Alert.
</Alert>
),
leftArea: (
<>
{/** @slot Nav mobile */}
<MenuButton
onClick={onOpen}
sx={{ mr: 1, ml: -1, [theme.breakpoints.up(layoutQuery)]: { display: 'none' } }}
/>
<NavMobile data={navData} open={open} onClose={onClose} workspaces={_workspaces} />
</>
),
rightArea: (
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 0, sm: 0.75 } }}>
{/** @slot Searchbar */}
<Searchbar />
{/** @slot Language popover */}
<LanguagePopover data={_langs} />
{/** @slot Notifications popover */}
<NotificationsPopover data={_notifications} />
{/** @slot Account drawer */}
<AccountPopover data={_account} />
</Box>
),
};
return (
<HeaderSection
disableElevation
layoutQuery={layoutQuery}
{...slotProps?.header}
slots={{ ...headerSlots, ...slotProps?.header?.slots }}
slotProps={merge(headerSlotProps, slotProps?.header?.slotProps ?? {})}
sx={slotProps?.header?.sx}
/>
);
};
const renderFooter = () => null;
const renderMain = () => <MainSection {...slotProps?.main}>{children}</MainSection>;
return (
<LayoutSection
/** **************************************
* @Header
*************************************** */
headerSection={renderHeader()}
/** **************************************
* @Sidebar
*************************************** */
sidebarSection={
<NavDesktop data={navData} layoutQuery={layoutQuery} workspaces={_workspaces} />
}
/** **************************************
* @Footer
*************************************** */
footerSection={renderFooter()}
/** **************************************
* @Styles
*************************************** */
cssVars={{ ...dashboardLayoutVars(theme), ...cssVars }}
sx={[
{
[`& .${layoutClasses.sidebarContainer}`]: {
[theme.breakpoints.up(layoutQuery)]: {
pl: 'var(--layout-nav-vertical-width)',
transition: theme.transitions.create(['padding-left'], {
easing: 'var(--layout-transition-easing)',
duration: 'var(--layout-transition-duration)',
}),
},
},
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
{renderMain()}
</LayoutSection>
);
}

View File

@@ -0,0 +1,194 @@
import type { Theme, SxProps, Breakpoint } from '@mui/material/styles';
import { useEffect } from 'react';
import { varAlpha } from 'minimal-shared/utils';
import Box from '@mui/material/Box';
import ListItem from '@mui/material/ListItem';
import { useTheme } from '@mui/material/styles';
import ListItemButton from '@mui/material/ListItemButton';
import Drawer, { drawerClasses } from '@mui/material/Drawer';
import { usePathname } from 'src/routes/hooks';
import { RouterLink } from 'src/routes/components';
import { Logo } from 'src/components/logo';
import { Scrollbar } from 'src/components/scrollbar';
import { NavUpgrade } from '../components/nav-upgrade';
import { WorkspacesPopover } from '../components/workspaces-popover';
import type { NavItem } from '../nav-config-dashboard';
import type { WorkspacesPopoverProps } from '../components/workspaces-popover';
// ----------------------------------------------------------------------
export type NavContentProps = {
data: NavItem[];
slots?: {
topArea?: React.ReactNode;
bottomArea?: React.ReactNode;
};
workspaces: WorkspacesPopoverProps['data'];
sx?: SxProps<Theme>;
};
export function NavDesktop({
sx,
data,
slots,
workspaces,
layoutQuery,
}: NavContentProps & { layoutQuery: Breakpoint }) {
const theme = useTheme();
return (
<Box
sx={{
pt: 2.5,
px: 2.5,
top: 0,
left: 0,
height: 1,
display: 'none',
position: 'fixed',
flexDirection: 'column',
zIndex: 'var(--layout-nav-zIndex)',
width: 'var(--layout-nav-vertical-width)',
borderRight: `1px solid ${varAlpha(theme.vars.palette.grey['500Channel'], 0.12)}`,
[theme.breakpoints.up(layoutQuery)]: {
display: 'flex',
},
...sx,
}}
>
<NavContent data={data} slots={slots} workspaces={workspaces} />
</Box>
);
}
// ----------------------------------------------------------------------
export function NavMobile({
sx,
data,
open,
slots,
onClose,
workspaces,
}: NavContentProps & { open: boolean; onClose: () => void }) {
const pathname = usePathname();
useEffect(() => {
if (open) {
onClose();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);
return (
<Drawer
open={open}
onClose={onClose}
sx={{
[`& .${drawerClasses.paper}`]: {
pt: 2.5,
px: 2.5,
overflow: 'unset',
width: 'var(--layout-nav-mobile-width)',
...sx,
},
}}
>
<NavContent data={data} slots={slots} workspaces={workspaces} />
</Drawer>
);
}
// ----------------------------------------------------------------------
export function NavContent({ data, slots, workspaces, sx }: NavContentProps) {
const pathname = usePathname();
return (
<>
<Logo />
{slots?.topArea}
<WorkspacesPopover data={workspaces} sx={{ my: 2 }} />
<Scrollbar fillContent>
<Box
component="nav"
sx={[
{
display: 'flex',
flex: '1 1 auto',
flexDirection: 'column',
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
<Box
component="ul"
sx={{
gap: 0.5,
display: 'flex',
flexDirection: 'column',
}}
>
{data.map((item) => {
const isActived = item.path === pathname;
return (
<ListItem disableGutters disablePadding key={item.title}>
<ListItemButton
disableGutters
component={RouterLink}
href={item.path}
sx={[
(theme) => ({
pl: 2,
py: 1,
gap: 2,
pr: 1.5,
borderRadius: 0.75,
typography: 'body2',
fontWeight: 'fontWeightMedium',
color: theme.vars.palette.text.secondary,
minHeight: 44,
...(isActived && {
fontWeight: 'fontWeightSemiBold',
color: theme.vars.palette.primary.main,
bgcolor: varAlpha(theme.vars.palette.primary.mainChannel, 0.08),
'&:hover': {
bgcolor: varAlpha(theme.vars.palette.primary.mainChannel, 0.16),
},
}),
}),
]}
>
<Box component="span" sx={{ width: 24, height: 24 }}>
{item.icon}
</Box>
<Box component="span" sx={{ flexGrow: 1 }}>
{item.title}
</Box>
{item.info && item.info}
</ListItemButton>
</ListItem>
);
})}
</Box>
</Box>
</Scrollbar>
{slots?.bottomArea}
<NavUpgrade />
</>
);
}

View File

@@ -0,0 +1,23 @@
import { Iconify } from 'src/components/iconify';
import type { AccountPopoverProps } from './components/account-popover';
// ----------------------------------------------------------------------
export const _account: AccountPopoverProps['data'] = [
{
label: 'Home',
href: '/',
icon: <Iconify width={22} icon="solar:home-angle-bold-duotone" />,
},
{
label: 'Profile',
href: '#',
icon: <Iconify width={22} icon="solar:shield-keyhole-bold-duotone" />,
},
{
label: 'Settings',
href: '#',
icon: <Iconify width={22} icon="solar:settings-bold-duotone" />,
},
];

View File

@@ -0,0 +1,51 @@
import { Label } from 'src/components/label';
import { SvgColor } from 'src/components/svg-color';
// ----------------------------------------------------------------------
const icon = (name: string) => <SvgColor src={`/assets/icons/navbar/${name}.svg`} />;
export type NavItem = {
title: string;
path: string;
icon: React.ReactNode;
info?: React.ReactNode;
};
export const navData = [
{
title: 'Dashboard',
path: '/',
icon: icon('ic-analytics'),
},
{
title: 'User',
path: '/user',
icon: icon('ic-user'),
},
{
title: 'Product',
path: '/products',
icon: icon('ic-cart'),
info: (
<Label color="error" variant="inverted">
+3
</Label>
),
},
{
title: 'Blog',
path: '/blog',
icon: icon('ic-blog'),
},
{
title: 'Sign in',
path: '/sign-in',
icon: icon('ic-lock'),
},
{
title: 'Not found',
path: '/404',
icon: icon('ic-disabled'),
},
];

View File

@@ -0,0 +1,24 @@
import type { WorkspacesPopoverProps } from './components/workspaces-popover';
// ----------------------------------------------------------------------
export const _workspaces: WorkspacesPopoverProps['data'] = [
{
id: 'team-1',
name: 'Team 1',
plan: 'Free',
logo: '/assets/icons/workspaces/logo-1.webp',
},
{
id: 'team-2',
name: 'Team 2',
plan: 'Pro',
logo: '/assets/icons/workspaces/logo-2.webp',
},
{
id: 'team-3',
name: 'Team 3',
plan: 'Pro',
logo: '/assets/icons/workspaces/logo-3.webp',
},
];

View File

@@ -0,0 +1,29 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Outlet, RouterProvider, createBrowserRouter } from 'react-router';
import App from './app';
import { routesSection } from './routes/sections';
import { ErrorBoundary } from './routes/components';
// ----------------------------------------------------------------------
const router = createBrowserRouter([
{
Component: () => (
<App>
<Outlet />
</App>
),
errorElement: <ErrorBoundary />,
children: routesSection,
},
]);
const root = createRoot(document.getElementById('root')!);
root.render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
);

View File

@@ -0,0 +1,16 @@
import { _posts } from 'src/_mock';
import { CONFIG } from 'src/config-global';
import { BlogView } from 'src/sections/blog/view';
// ----------------------------------------------------------------------
export default function Page() {
return (
<>
<title>{`Blog - ${CONFIG.appName}`}</title>
<BlogView posts={_posts} />
</>
);
}

View File

@@ -0,0 +1,20 @@
import { CONFIG } from 'src/config-global';
import { OverviewAnalyticsView as DashboardView } from 'src/sections/overview/view';
// ----------------------------------------------------------------------
export default function Page() {
return (
<>
<title>{`Dashboard - ${CONFIG.appName}`}</title>
<meta
name="description"
content="The starting point for your next project with Minimal UI Kit, built on the newest version of Material-UI ©, ready to be customized to your style"
/>
<meta name="keywords" content="react,material,kit,application,dashboard,admin,template" />
<DashboardView />
</>
);
}

View File

@@ -0,0 +1,15 @@
import { CONFIG } from 'src/config-global';
import { NotFoundView } from 'src/sections/error';
// ----------------------------------------------------------------------
export default function Page() {
return (
<>
<title>{`404 page not found! | Error - ${CONFIG.appName}`}</title>
<NotFoundView />
</>
);
}

View File

@@ -0,0 +1,15 @@
import { CONFIG } from 'src/config-global';
import { ProductsView } from 'src/sections/product/view';
// ----------------------------------------------------------------------
export default function Page() {
return (
<>
<title>{`Products - ${CONFIG.appName}`}</title>
<ProductsView />
</>
);
}

View File

@@ -0,0 +1,15 @@
import { CONFIG } from 'src/config-global';
import { SignInView } from 'src/sections/auth';
// ----------------------------------------------------------------------
export default function Page() {
return (
<>
<title>{`Sign in - ${CONFIG.appName}`}</title>
<SignInView />
</>
);
}

View File

@@ -0,0 +1,15 @@
import { CONFIG } from 'src/config-global';
import { UserView } from 'src/sections/user/view';
// ----------------------------------------------------------------------
export default function Page() {
return (
<>
<title>{`Users - ${CONFIG.appName}`}</title>
<UserView />
</>
);
}

View File

@@ -0,0 +1,168 @@
import type { Theme, CSSObject } from '@mui/material/styles';
import { useRouteError, isRouteErrorResponse } from 'react-router';
import GlobalStyles from '@mui/material/GlobalStyles';
// ----------------------------------------------------------------------
export function ErrorBoundary() {
const error = useRouteError();
return (
<>
{inputGlobalStyles()}
<div className={errorBoundaryClasses.root}>
<div className={errorBoundaryClasses.container}>{renderErrorMessage(error)}</div>
</div>
</>
);
}
// ----------------------------------------------------------------------
function parseStackTrace(stack?: string) {
if (!stack) return { filePath: null, functionName: null };
const filePathMatch = stack.match(/\/src\/[^?]+/);
const functionNameMatch = stack.match(/at (\S+)/);
return {
filePath: filePathMatch ? filePathMatch[0] : null,
functionName: functionNameMatch ? functionNameMatch[1] : null,
};
}
function renderErrorMessage(error: any) {
if (isRouteErrorResponse(error)) {
return (
<>
<h1 className={errorBoundaryClasses.title}>
{error.status}: {error.statusText}
</h1>
<p className={errorBoundaryClasses.message}>{error.data}</p>
</>
);
}
if (error instanceof Error) {
const { filePath, functionName } = parseStackTrace(error.stack);
return (
<>
<h1 className={errorBoundaryClasses.title}>Unexpected Application Error!</h1>
<p className={errorBoundaryClasses.message}>
{error.name}: {error.message}
</p>
<pre className={errorBoundaryClasses.details}>{error.stack}</pre>
{(filePath || functionName) && (
<p className={errorBoundaryClasses.filePath}>
{filePath} ({functionName})
</p>
)}
</>
);
}
return <h1 className={errorBoundaryClasses.title}>Unknown Error</h1>;
}
// ----------------------------------------------------------------------
const errorBoundaryClasses = {
root: 'error-boundary-root',
container: 'error-boundary-container',
title: 'error-boundary-title',
details: 'error-boundary-details',
message: 'error-boundary-message',
filePath: 'error-boundary-file-path',
};
const cssVars: CSSObject = {
'--info-color': '#2dd9da',
'--warning-color': '#e2aa53',
'--error-color': '#ff5555',
'--error-background': '#2a1e1e',
'--details-background': '#111111',
'--root-background': '#2c2c2e',
'--container-background': '#1c1c1e',
'--font-stack-monospace':
'"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace',
'--font-stack-sans':
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
};
const rootStyles = (): CSSObject => ({
display: 'flex',
flex: '1 1 auto',
alignItems: 'center',
padding: '10vh 15px 0',
flexDirection: 'column',
fontFamily: 'var(--font-stack-sans)',
});
const contentStyles = (): CSSObject => ({
gap: 24,
padding: 20,
width: '100%',
maxWidth: 960,
display: 'flex',
borderRadius: 8,
flexDirection: 'column',
backgroundColor: 'var(--container-background)',
});
const titleStyles = (theme: Theme): CSSObject => ({
margin: 0,
lineHeight: 1.2,
fontSize: theme.typography.pxToRem(20),
fontWeight: theme.typography.fontWeightBold,
});
const messageStyles = (theme: Theme): CSSObject => ({
margin: 0,
lineHeight: 1.5,
padding: '12px 16px',
whiteSpace: 'pre-wrap',
color: 'var(--error-color)',
fontSize: theme.typography.pxToRem(14),
fontFamily: 'var(--font-stack-monospace)',
backgroundColor: 'var(--error-background)',
borderLeft: '2px solid var(--error-color)',
fontWeight: theme.typography.fontWeightBold,
});
const detailsStyles = (): CSSObject => ({
margin: 0,
padding: 16,
lineHeight: 1.5,
overflow: 'auto',
borderRadius: 'inherit',
color: 'var(--warning-color)',
backgroundColor: 'var(--details-background)',
});
const filePathStyles = (): CSSObject => ({
marginTop: 0,
color: 'var(--info-color)',
});
const inputGlobalStyles = () => (
<GlobalStyles
styles={(theme) => ({
body: {
...cssVars,
margin: 0,
color: 'white',
backgroundColor: 'var(--root-background)',
[`& .${errorBoundaryClasses.root}`]: rootStyles(),
[`& .${errorBoundaryClasses.container}`]: contentStyles(),
[`& .${errorBoundaryClasses.title}`]: titleStyles(theme),
[`& .${errorBoundaryClasses.message}`]: messageStyles(theme),
[`& .${errorBoundaryClasses.filePath}`]: filePathStyles(),
[`& .${errorBoundaryClasses.details}`]: detailsStyles(),
},
})}
/>
);

View File

@@ -0,0 +1,3 @@
export * from './router-link';
export * from './error-boundary';

View File

@@ -0,0 +1,14 @@
import type { LinkProps } from 'react-router';
import { Link } from 'react-router';
// ----------------------------------------------------------------------
interface RouterLinkProps extends Omit<LinkProps, 'to'> {
href: string;
ref?: React.RefObject<HTMLAnchorElement | null>;
}
export function RouterLink({ href, ref, ...other }: RouterLinkProps) {
return <Link ref={ref} to={href} {...other} />;
}

View File

@@ -0,0 +1,3 @@
export { useRouter } from './use-router';
export { usePathname } from './use-pathname';

View File

@@ -0,0 +1,10 @@
import { useMemo } from 'react';
import { useLocation } from 'react-router';
// ----------------------------------------------------------------------
export function usePathname() {
const { pathname } = useLocation();
return useMemo(() => pathname, [pathname]);
}

View File

@@ -0,0 +1,21 @@
import { useMemo } from 'react';
import { useNavigate } from 'react-router';
// ----------------------------------------------------------------------
export function useRouter() {
const navigate = useNavigate();
const router = useMemo(
() => ({
back: () => navigate(-1),
forward: () => navigate(1),
refresh: () => navigate(0),
push: (href: string) => navigate(href),
replace: (href: string) => navigate(href, { replace: true }),
}),
[navigate]
);
return router;
}

View File

@@ -0,0 +1,71 @@
import type { RouteObject } from 'react-router';
import { lazy, Suspense } from 'react';
import { Outlet } from 'react-router-dom';
import { varAlpha } from 'minimal-shared/utils';
import Box from '@mui/material/Box';
import LinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress';
import { AuthLayout } from 'src/layouts/auth';
import { DashboardLayout } from 'src/layouts/dashboard';
// ----------------------------------------------------------------------
export const DashboardPage = lazy(() => import('src/pages/dashboard'));
export const BlogPage = lazy(() => import('src/pages/blog'));
export const UserPage = lazy(() => import('src/pages/user'));
export const SignInPage = lazy(() => import('src/pages/sign-in'));
export const ProductsPage = lazy(() => import('src/pages/products'));
export const Page404 = lazy(() => import('src/pages/page-not-found'));
const renderFallback = () => (
<Box
sx={{
display: 'flex',
flex: '1 1 auto',
alignItems: 'center',
justifyContent: 'center',
}}
>
<LinearProgress
sx={{
width: 1,
maxWidth: 320,
bgcolor: (theme) => varAlpha(theme.vars.palette.text.primaryChannel, 0.16),
[`& .${linearProgressClasses.bar}`]: { bgcolor: 'text.primary' },
}}
/>
</Box>
);
export const routesSection: RouteObject[] = [
{
element: (
<DashboardLayout>
<Suspense fallback={renderFallback()}>
<Outlet />
</Suspense>
</DashboardLayout>
),
children: [
{ index: true, element: <DashboardPage /> },
{ path: 'user', element: <UserPage /> },
{ path: 'products', element: <ProductsPage /> },
{ path: 'blog', element: <BlogPage /> },
],
},
{
path: 'sign-in',
element: (
<AuthLayout>
<SignInPage />
</AuthLayout>
),
},
{
path: '404',
element: <Page404 />,
},
{ path: '*', element: <Page404 /> },
];

View File

@@ -0,0 +1 @@
export * from './sign-in-view';

View File

@@ -0,0 +1,136 @@
import { useState, useCallback } from 'react';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import TextField from '@mui/material/TextField';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import InputAdornment from '@mui/material/InputAdornment';
import { useRouter } from 'src/routes/hooks';
import { Iconify } from 'src/components/iconify';
// ----------------------------------------------------------------------
export function SignInView() {
const router = useRouter();
const [showPassword, setShowPassword] = useState(false);
const handleSignIn = useCallback(() => {
router.push('/');
}, [router]);
const renderForm = (
<Box
sx={{
display: 'flex',
alignItems: 'flex-end',
flexDirection: 'column',
}}
>
<TextField
fullWidth
name="email"
label="Email address"
defaultValue="hello@gmail.com"
sx={{ mb: 3 }}
slotProps={{
inputLabel: { shrink: true },
}}
/>
<Link variant="body2" color="inherit" sx={{ mb: 1.5 }}>
Forgot password?
</Link>
<TextField
fullWidth
name="password"
label="Password"
defaultValue="@demo1234"
type={showPassword ? 'text' : 'password'}
slotProps={{
inputLabel: { shrink: true },
input: {
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowPassword(!showPassword)} edge="end">
<Iconify icon={showPassword ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
</IconButton>
</InputAdornment>
),
},
}}
sx={{ mb: 3 }}
/>
<Button
fullWidth
size="large"
type="submit"
color="inherit"
variant="contained"
onClick={handleSignIn}
>
Sign in
</Button>
</Box>
);
return (
<>
<Box
sx={{
gap: 1.5,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
mb: 5,
}}
>
<Typography variant="h5">Sign in</Typography>
<Typography
variant="body2"
sx={{
color: 'text.secondary',
}}
>
Dont have an account?
<Link variant="subtitle2" sx={{ ml: 0.5 }}>
Get started
</Link>
</Typography>
</Box>
{renderForm}
<Divider sx={{ my: 3, '&::before, &::after': { borderTopStyle: 'dashed' } }}>
<Typography
variant="overline"
sx={{ color: 'text.secondary', fontWeight: 'fontWeightMedium' }}
>
OR
</Typography>
</Divider>
<Box
sx={{
gap: 1,
display: 'flex',
justifyContent: 'center',
}}
>
<IconButton color="inherit">
<Iconify width={22} icon="socials:google" />
</IconButton>
<IconButton color="inherit">
<Iconify width={22} icon="socials:github" />
</IconButton>
<IconButton color="inherit">
<Iconify width={22} icon="socials:twitter" />
</IconButton>
</Box>
</>
);
}

View File

@@ -0,0 +1,211 @@
import type { CardProps } from '@mui/material/Card';
import type { IconifyName } from 'src/components/iconify';
import { varAlpha } from 'minimal-shared/utils';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Card from '@mui/material/Card';
import Avatar from '@mui/material/Avatar';
import Typography from '@mui/material/Typography';
import { fDate } from 'src/utils/format-time';
import { fShortenNumber } from 'src/utils/format-number';
import { Iconify } from 'src/components/iconify';
import { SvgColor } from 'src/components/svg-color';
// ----------------------------------------------------------------------
export type IPostItem = {
id: string;
title: string;
coverUrl: string;
totalViews: number;
description: string;
totalShares: number;
totalComments: number;
totalFavorites: number;
postedAt: string | number | null;
author: {
name: string;
avatarUrl: string;
};
};
export function PostItem({
sx,
post,
latestPost,
latestPostLarge,
...other
}: CardProps & {
post: IPostItem;
latestPost: boolean;
latestPostLarge: boolean;
}) {
const renderAvatar = (
<Avatar
alt={post.author.name}
src={post.author.avatarUrl}
sx={{
left: 24,
zIndex: 9,
bottom: -24,
position: 'absolute',
...((latestPostLarge || latestPost) && {
top: 24,
}),
}}
/>
);
const renderTitle = (
<Link
color="inherit"
variant="subtitle2"
underline="hover"
sx={{
height: 44,
overflow: 'hidden',
WebkitLineClamp: 2,
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
...(latestPostLarge && { typography: 'h5', height: 60 }),
...((latestPostLarge || latestPost) && {
color: 'common.white',
}),
}}
>
{post.title}
</Link>
);
const renderInfo = (
<Box
sx={{
mt: 3,
gap: 1.5,
display: 'flex',
flexWrap: 'wrap',
color: 'text.disabled',
justifyContent: 'flex-end',
}}
>
{[
{ number: post.totalComments, icon: 'solar:chat-round-dots-bold' },
{ number: post.totalViews, icon: 'solar:eye-bold' },
{ number: post.totalShares, icon: 'solar:share-bold' },
].map((info, _index) => (
<Box
key={_index}
sx={{
display: 'flex',
...((latestPostLarge || latestPost) && {
opacity: 0.64,
color: 'common.white',
}),
}}
>
<Iconify width={16} icon={info.icon as IconifyName} sx={{ mr: 0.5 }} />
<Typography variant="caption">{fShortenNumber(info.number)}</Typography>
</Box>
))}
</Box>
);
const renderCover = (
<Box
component="img"
alt={post.title}
src={post.coverUrl}
sx={{
top: 0,
width: 1,
height: 1,
objectFit: 'cover',
position: 'absolute',
}}
/>
);
const renderDate = (
<Typography
variant="caption"
component="div"
sx={{
mb: 1,
color: 'text.disabled',
...((latestPostLarge || latestPost) && {
opacity: 0.48,
color: 'common.white',
}),
}}
>
{fDate(post.postedAt)}
</Typography>
);
const renderShape = (
<SvgColor
src="/assets/icons/shape-avatar.svg"
sx={{
left: 0,
width: 88,
zIndex: 9,
height: 36,
bottom: -16,
position: 'absolute',
color: 'background.paper',
...((latestPostLarge || latestPost) && { display: 'none' }),
}}
/>
);
return (
<Card sx={sx} {...other}>
<Box
sx={(theme) => ({
position: 'relative',
pt: 'calc(100% * 3 / 4)',
...((latestPostLarge || latestPost) && {
pt: 'calc(100% * 4 / 3)',
'&:after': {
top: 0,
content: "''",
width: '100%',
height: '100%',
position: 'absolute',
bgcolor: varAlpha(theme.palette.grey['900Channel'], 0.72),
},
}),
...(latestPostLarge && {
pt: {
xs: 'calc(100% * 4 / 3)',
sm: 'calc(100% * 3 / 4.66)',
},
}),
})}
>
{renderShape}
{renderAvatar}
{renderCover}
</Box>
<Box
sx={(theme) => ({
p: theme.spacing(6, 3, 3, 3),
...((latestPostLarge || latestPost) && {
width: 1,
bottom: 0,
position: 'absolute',
}),
})}
>
{renderDate}
{renderTitle}
{renderInfo}
</Box>
</Card>
);
}

View File

@@ -0,0 +1,59 @@
import type { Theme, SxProps } from '@mui/material/styles';
import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete';
import { Iconify } from 'src/components/iconify';
import type { IPostItem } from './post-item';
// ----------------------------------------------------------------------
type PostSearchProps = {
posts: IPostItem[];
sx?: SxProps<Theme>;
};
export function PostSearch({ posts, sx }: PostSearchProps) {
return (
<Autocomplete
sx={{ width: 280 }}
autoHighlight
popupIcon={null}
slotProps={{
paper: {
sx: {
width: 320,
[`& .${autocompleteClasses.option}`]: {
typography: 'body2',
},
...sx,
},
},
}}
options={posts}
getOptionLabel={(post) => post.title}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderInput={(params) => (
<TextField
{...params}
placeholder="Search post..."
slotProps={{
input: {
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<Iconify
icon="eva:search-fill"
sx={{ ml: 1, width: 20, height: 20, color: 'text.disabled' }}
/>
</InputAdornment>
),
},
}}
/>
)}
/>
);
}

View File

@@ -0,0 +1,96 @@
import type { ButtonProps } from '@mui/material/Button';
import { useState, useCallback } from 'react';
import { varAlpha } from 'minimal-shared/utils';
import Button from '@mui/material/Button';
import Popover from '@mui/material/Popover';
import MenuList from '@mui/material/MenuList';
import MenuItem, { menuItemClasses } from '@mui/material/MenuItem';
import { Iconify } from 'src/components/iconify';
// ----------------------------------------------------------------------
type PostSortProps = ButtonProps & {
sortBy: string;
onSort: (newSort: string) => void;
options: { value: string; label: string }[];
};
export function PostSort({ options, sortBy, onSort, sx, ...other }: PostSortProps) {
const [openPopover, setOpenPopover] = useState<HTMLButtonElement | null>(null);
const handleOpenPopover = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
setOpenPopover(event.currentTarget);
}, []);
const handleClosePopover = useCallback(() => {
setOpenPopover(null);
}, []);
return (
<>
<Button
disableRipple
color="inherit"
onClick={handleOpenPopover}
endIcon={
<Iconify
icon={openPopover ? 'eva:arrow-ios-upward-fill' : 'eva:arrow-ios-downward-fill'}
sx={{
ml: -0.5,
}}
/>
}
sx={[
{
bgcolor: (theme) => varAlpha(theme.vars.palette.grey['500Channel'], 0.08),
},
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
{options.find((option) => option.value === sortBy)?.label}
</Button>
<Popover
open={!!openPopover}
anchorEl={openPopover}
onClose={handleClosePopover}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<MenuList
disablePadding
sx={{
p: 0.5,
gap: 0.5,
width: 160,
display: 'flex',
flexDirection: 'column',
[`& .${menuItemClasses.root}`]: {
px: 1,
gap: 2,
borderRadius: 0.75,
[`&.${menuItemClasses.selected}`]: { bgcolor: 'action.selected' },
},
}}
>
{options.map((option) => (
<MenuItem
key={option.value}
selected={option.value === sortBy}
onClick={() => {
onSort(option.value);
handleClosePopover();
}}
>
{option.label}
</MenuItem>
))}
</MenuList>
</Popover>
</>
);
}

View File

@@ -0,0 +1,96 @@
import { useState, useCallback } from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import Pagination from '@mui/material/Pagination';
import { DashboardContent } from 'src/layouts/dashboard';
import { Iconify } from 'src/components/iconify';
import { PostItem } from '../post-item';
import { PostSort } from '../post-sort';
import { PostSearch } from '../post-search';
import type { IPostItem } from '../post-item';
// ----------------------------------------------------------------------
type Props = {
posts: IPostItem[];
};
export function BlogView({ posts }: Props) {
const [sortBy, setSortBy] = useState('latest');
const handleSort = useCallback((newSort: string) => {
setSortBy(newSort);
}, []);
return (
<DashboardContent>
<Box
sx={{
mb: 5,
display: 'flex',
alignItems: 'center',
}}
>
<Typography variant="h4" sx={{ flexGrow: 1 }}>
Blog
</Typography>
<Button
variant="contained"
color="inherit"
startIcon={<Iconify icon="mingcute:add-line" />}
>
New post
</Button>
</Box>
<Box
sx={{
mb: 5,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<PostSearch posts={posts} />
<PostSort
sortBy={sortBy}
onSort={handleSort}
options={[
{ value: 'latest', label: 'Latest' },
{ value: 'popular', label: 'Popular' },
{ value: 'oldest', label: 'Oldest' },
]}
/>
</Box>
<Grid container spacing={3}>
{posts.map((post, index) => {
const latestPostLarge = index === 0;
const latestPost = index === 1 || index === 2;
return (
<Grid
key={post.id}
size={{
xs: 12,
sm: latestPostLarge ? 12 : 6,
md: latestPostLarge ? 6 : 3,
}}
>
<PostItem post={post} latestPost={latestPost} latestPostLarge={latestPostLarge} />
</Grid>
);
})}
</Grid>
<Pagination count={10} color="primary" sx={{ mt: 8, mx: 'auto' }} />
</DashboardContent>
);
}

View File

@@ -0,0 +1 @@
export * from './blog-view';

View File

@@ -0,0 +1 @@
export * from './not-found-view';

View File

@@ -0,0 +1,52 @@
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Container from '@mui/material/Container';
import Typography from '@mui/material/Typography';
import { RouterLink } from 'src/routes/components';
import { Logo } from 'src/components/logo';
// ----------------------------------------------------------------------
export function NotFoundView() {
return (
<>
<Logo sx={{ position: 'fixed', top: 20, left: 20 }} />
<Container
sx={{
py: 10,
flexGrow: 1,
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'center',
}}
>
<Typography variant="h3" sx={{ mb: 2 }}>
Sorry, page not found!
</Typography>
<Typography sx={{ color: 'text.secondary', maxWidth: 480, textAlign: 'center' }}>
Sorry, we couldnt find the page youre looking for. Perhaps youve mistyped the URL? Be
sure to check your spelling.
</Typography>
<Box
component="img"
src="/assets/illustrations/illustration-404.svg"
sx={{
width: 320,
height: 'auto',
my: { xs: 5, sm: 10 },
}}
/>
<Button component={RouterLink} href="/" size="large" variant="contained" color="inherit">
Go to home
</Button>
</Container>
</>
);
}

View File

@@ -0,0 +1,82 @@
import type { CardProps } from '@mui/material/Card';
import type { ChartOptions } from 'src/components/chart';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import { useTheme, alpha as hexAlpha } from '@mui/material/styles';
import { fNumber } from 'src/utils/format-number';
import { Chart, useChart } from 'src/components/chart';
// ----------------------------------------------------------------------
type Props = CardProps & {
title?: string;
subheader?: string;
chart: {
colors?: string[];
categories?: string[];
series: {
name: string;
data: number[];
}[];
options?: ChartOptions;
};
};
export function AnalyticsConversionRates({ title, subheader, chart, sx, ...other }: Props) {
const theme = useTheme();
const chartColors = chart.colors ?? [
theme.palette.primary.dark,
hexAlpha(theme.palette.primary.dark, 0.24),
];
const chartOptions = useChart({
colors: chartColors,
stroke: { width: 2, colors: ['transparent'] },
tooltip: {
shared: true,
intersect: false,
y: {
formatter: (value: number) => fNumber(value),
title: { formatter: (seriesName: string) => `${seriesName}: ` },
},
},
xaxis: { categories: chart.categories },
dataLabels: {
enabled: true,
offsetX: -6,
style: { fontSize: '10px', colors: ['#FFFFFF', theme.palette.text.primary] },
},
plotOptions: {
bar: {
horizontal: true,
borderRadius: 2,
barHeight: '48%',
dataLabels: { position: 'top' },
},
},
...chart.options,
});
return (
<Card sx={sx} {...other}>
<CardHeader title={title} subheader={subheader} />
<Chart
type="bar"
series={chart.series}
options={chartOptions}
slotProps={{ loading: { p: 2.5 } }}
sx={{
pl: 1,
py: 2.5,
pr: 2.5,
height: 360,
}}
/>
</Card>
);
}

View File

@@ -0,0 +1,73 @@
import type { CardProps } from '@mui/material/Card';
import type { ChartOptions } from 'src/components/chart';
import Card from '@mui/material/Card';
import Divider from '@mui/material/Divider';
import { useTheme } from '@mui/material/styles';
import CardHeader from '@mui/material/CardHeader';
import { Chart, useChart, ChartLegends } from 'src/components/chart';
// ----------------------------------------------------------------------
type Props = CardProps & {
title?: string;
subheader?: string;
chart: {
colors?: string[];
categories: string[];
series: {
name: string;
data: number[];
}[];
options?: ChartOptions;
};
};
export function AnalyticsCurrentSubject({ title, subheader, chart, sx, ...other }: Props) {
const theme = useTheme();
const chartColors = chart.colors ?? [
theme.palette.primary.main,
theme.palette.warning.main,
theme.palette.info.main,
];
const chartOptions = useChart({
colors: chartColors,
stroke: { width: 2 },
fill: { opacity: 0.48 },
xaxis: {
categories: chart.categories,
labels: { style: { colors: Array.from({ length: 6 }, () => theme.palette.text.secondary) } },
},
...chart.options,
});
return (
<Card sx={sx} {...other}>
<CardHeader title={title} subheader={subheader} />
<Chart
type="radar"
series={chart.series}
options={chartOptions}
slotProps={{ loading: { py: 2.5 } }}
sx={{
my: 1,
mx: 'auto',
width: 300,
height: 300,
}}
/>
<Divider sx={{ borderStyle: 'dashed' }} />
<ChartLegends
labels={chart.series.map((item) => item.name)}
colors={chartOptions?.colors}
sx={{ p: 3, justifyContent: 'center' }}
/>
</Card>
);
}

View File

@@ -0,0 +1,81 @@
import type { CardProps } from '@mui/material/Card';
import type { ChartOptions } from 'src/components/chart';
import Card from '@mui/material/Card';
import Divider from '@mui/material/Divider';
import { useTheme } from '@mui/material/styles';
import CardHeader from '@mui/material/CardHeader';
import { fNumber } from 'src/utils/format-number';
import { Chart, useChart, ChartLegends } from 'src/components/chart';
// ----------------------------------------------------------------------
type Props = CardProps & {
title?: string;
subheader?: string;
chart: {
colors?: string[];
series: {
label: string;
value: number;
}[];
options?: ChartOptions;
};
};
export function AnalyticsCurrentVisits({ title, subheader, chart, sx, ...other }: Props) {
const theme = useTheme();
const chartSeries = chart.series.map((item) => item.value);
const chartColors = chart.colors ?? [
theme.palette.primary.main,
theme.palette.warning.light,
theme.palette.info.dark,
theme.palette.error.main,
];
const chartOptions = useChart({
chart: { sparkline: { enabled: true } },
colors: chartColors,
labels: chart.series.map((item) => item.label),
stroke: { width: 0 },
dataLabels: { enabled: true, dropShadow: { enabled: false } },
tooltip: {
y: {
formatter: (value: number) => fNumber(value),
title: { formatter: (seriesName: string) => `${seriesName}` },
},
},
plotOptions: { pie: { donut: { labels: { show: false } } } },
...chart.options,
});
return (
<Card sx={sx} {...other}>
<CardHeader title={title} subheader={subheader} />
<Chart
type="pie"
series={chartSeries}
options={chartOptions}
sx={{
my: 6,
mx: 'auto',
width: { xs: 240, xl: 260 },
height: { xs: 240, xl: 260 },
}}
/>
<Divider sx={{ borderStyle: 'dashed' }} />
<ChartLegends
labels={chartOptions?.labels}
colors={chartOptions?.colors}
sx={{ p: 3, justifyContent: 'center' }}
/>
</Card>
);
}

View File

@@ -0,0 +1,103 @@
import type { BoxProps } from '@mui/material/Box';
import type { CardProps } from '@mui/material/Card';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Card from '@mui/material/Card';
import Button from '@mui/material/Button';
import Avatar from '@mui/material/Avatar';
import CardHeader from '@mui/material/CardHeader';
import ListItemText from '@mui/material/ListItemText';
import { fToNow } from 'src/utils/format-time';
import { Iconify } from 'src/components/iconify';
import { Scrollbar } from 'src/components/scrollbar';
// ----------------------------------------------------------------------
type Props = CardProps & {
title?: string;
subheader?: string;
list: {
id: string;
title: string;
coverUrl: string;
description: string;
postedAt: string | number | null;
}[];
};
export function AnalyticsNews({ title, subheader, list, sx, ...other }: Props) {
return (
<Card sx={sx} {...other}>
<CardHeader title={title} subheader={subheader} sx={{ mb: 1 }} />
<Scrollbar sx={{ minHeight: 405 }}>
<Box sx={{ minWidth: 640 }}>
{list.map((item) => (
<Item key={item.id} item={item} />
))}
</Box>
</Scrollbar>
<Box sx={{ p: 2, textAlign: 'right' }}>
<Button
size="small"
color="inherit"
endIcon={<Iconify icon="eva:arrow-ios-forward-fill" width={18} sx={{ ml: -0.5 }} />}
>
View all
</Button>
</Box>
</Card>
);
}
// ----------------------------------------------------------------------
type ItemProps = BoxProps & {
item: Props['list'][number];
};
function Item({ item, sx, ...other }: ItemProps) {
return (
<Box
sx={[
(theme) => ({
py: 2,
px: 3,
gap: 2,
display: 'flex',
alignItems: 'center',
borderBottom: `dashed 1px ${theme.vars.palette.divider}`,
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
<Avatar
variant="rounded"
alt={item.title}
src={item.coverUrl}
sx={{ width: 48, height: 48, flexShrink: 0 }}
/>
<ListItemText
primary={<Link color="inherit">{item.title}</Link>}
secondary={item.description}
slotProps={{
primary: { noWrap: true },
secondary: {
noWrap: true,
sx: { mt: 0.5 },
},
}}
/>
<Box sx={{ flexShrink: 0, typography: 'caption', color: 'text.disabled' }}>
{fToNow(item.postedAt)}
</Box>
</Box>
);
}

View File

@@ -0,0 +1,77 @@
import type { CardProps } from '@mui/material/Card';
import type { TimelineItemProps } from '@mui/lab/TimelineItem';
import Card from '@mui/material/Card';
import Timeline from '@mui/lab/Timeline';
import TimelineDot from '@mui/lab/TimelineDot';
import Typography from '@mui/material/Typography';
import CardHeader from '@mui/material/CardHeader';
import TimelineContent from '@mui/lab/TimelineContent';
import TimelineSeparator from '@mui/lab/TimelineSeparator';
import TimelineConnector from '@mui/lab/TimelineConnector';
import TimelineItem, { timelineItemClasses } from '@mui/lab/TimelineItem';
import { fDateTime } from 'src/utils/format-time';
// ----------------------------------------------------------------------
type Props = CardProps & {
title?: string;
subheader?: string;
list: {
id: string;
type: string;
title: string;
time: string | number | null;
}[];
};
export function AnalyticsOrderTimeline({ title, subheader, list, sx, ...other }: Props) {
return (
<Card sx={sx} {...other}>
<CardHeader title={title} subheader={subheader} />
<Timeline
sx={{ m: 0, p: 3, [`& .${timelineItemClasses.root}:before`]: { flex: 0, padding: 0 } }}
>
{list.map((item, index) => (
<Item key={item.id} item={item} lastItem={index === list.length - 1} />
))}
</Timeline>
</Card>
);
}
// ----------------------------------------------------------------------
type ItemProps = TimelineItemProps & {
lastItem: boolean;
item: Props['list'][number];
};
function Item({ item, lastItem, ...other }: ItemProps) {
return (
<TimelineItem {...other}>
<TimelineSeparator>
<TimelineDot
color={
(item.type === 'order1' && 'primary') ||
(item.type === 'order2' && 'success') ||
(item.type === 'order3' && 'info') ||
(item.type === 'order4' && 'warning') ||
'error'
}
/>
{lastItem ? null : <TimelineConnector />}
</TimelineSeparator>
<TimelineContent>
<Typography variant="subtitle2">{item.title}</Typography>
<Typography variant="caption" sx={{ color: 'text.disabled' }}>
{fDateTime(item.time)}
</Typography>
</TimelineContent>
</TimelineItem>
);
}

View File

@@ -0,0 +1,179 @@
import type { BoxProps } from '@mui/material/Box';
import type { CardProps } from '@mui/material/Card';
import { useState } from 'react';
import { usePopover } from 'minimal-shared/hooks';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import Stack from '@mui/material/Stack';
import Popover from '@mui/material/Popover';
import Divider from '@mui/material/Divider';
import MenuList from '@mui/material/MenuList';
import Checkbox from '@mui/material/Checkbox';
import IconButton from '@mui/material/IconButton';
import CardHeader from '@mui/material/CardHeader';
import FormControlLabel from '@mui/material/FormControlLabel';
import MenuItem, { menuItemClasses } from '@mui/material/MenuItem';
import { Iconify } from 'src/components/iconify';
import { Scrollbar } from 'src/components/scrollbar';
// ----------------------------------------------------------------------
type Props = CardProps & {
title?: string;
subheader?: string;
list: {
id: string;
name: string;
}[];
};
export function AnalyticsTasks({ title, subheader, list, sx, ...other }: Props) {
const [selected, setSelected] = useState(['2']);
const handleClickComplete = (taskId: string) => {
const tasksCompleted = selected.includes(taskId)
? selected.filter((value) => value !== taskId)
: [...selected, taskId];
setSelected(tasksCompleted);
};
return (
<Card sx={sx} {...other}>
<CardHeader title={title} subheader={subheader} sx={{ mb: 1 }} />
<Scrollbar sx={{ minHeight: 304 }}>
<Stack divider={<Divider sx={{ borderStyle: 'dashed' }} />} sx={{ minWidth: 560 }}>
{list.map((item) => (
<TaskItem
key={item.id}
item={item}
selected={selected.includes(item.id)}
onChange={() => handleClickComplete(item.id)}
/>
))}
</Stack>
</Scrollbar>
</Card>
);
}
// ----------------------------------------------------------------------
type TaskItemProps = BoxProps & {
selected: boolean;
item: Props['list'][number];
onChange: (id: string) => void;
};
function TaskItem({ item, selected, onChange, sx, ...other }: TaskItemProps) {
const menuActions = usePopover();
const handleMarkComplete = () => {
menuActions.onClose();
console.info('MARK COMPLETE', item.id);
};
const handleShare = () => {
menuActions.onClose();
console.info('SHARE', item.id);
};
const handleEdit = () => {
menuActions.onClose();
console.info('EDIT', item.id);
};
const handleDelete = () => {
menuActions.onClose();
console.info('DELETE', item.id);
};
return (
<>
<Box
sx={[
() => ({
pl: 2,
pr: 1,
py: 1.5,
display: 'flex',
...(selected && {
color: 'text.disabled',
textDecoration: 'line-through',
}),
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
<FormControlLabel
label={item.name}
control={
<Checkbox
disableRipple
checked={selected}
onChange={onChange}
slotProps={{ input: { id: `${item.name}-checkbox` } }}
/>
}
sx={{ flexGrow: 1, m: 0 }}
/>
<IconButton color={menuActions.open ? 'inherit' : 'default'} onClick={menuActions.onOpen}>
<Iconify icon="eva:more-vertical-fill" />
</IconButton>
</Box>
<Popover
open={menuActions.open}
anchorEl={menuActions.anchorEl}
onClose={menuActions.onClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<MenuList
disablePadding
sx={{
p: 0.5,
gap: 0.5,
display: 'flex',
flexDirection: 'column',
[`& .${menuItemClasses.root}`]: {
pl: 1,
pr: 2,
gap: 2,
borderRadius: 0.75,
[`&.${menuItemClasses.selected}`]: { bgcolor: 'action.selected' },
},
}}
>
<MenuItem onClick={handleMarkComplete}>
<Iconify icon="solar:check-circle-bold" />
Mark complete
</MenuItem>
<MenuItem onClick={handleEdit}>
<Iconify icon="solar:pen-bold" />
Edit
</MenuItem>
<MenuItem onClick={handleShare}>
<Iconify icon="solar:share-bold" />
Share
</MenuItem>
<Divider sx={{ borderStyle: 'dashed' }} />
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
<Iconify icon="solar:trash-bin-trash-bold" />
Delete
</MenuItem>
</MenuList>
</Popover>
</>
);
}

View File

@@ -0,0 +1,64 @@
import type { CardProps } from '@mui/material/Card';
import { varAlpha } from 'minimal-shared/utils';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import Typography from '@mui/material/Typography';
import { fShortenNumber } from 'src/utils/format-number';
import { Iconify } from 'src/components/iconify';
// ----------------------------------------------------------------------
type Props = CardProps & {
title?: string;
subheader?: string;
list: { value: string; label: string; total: number }[];
};
export function AnalyticsTrafficBySite({ title, subheader, list, sx, ...other }: Props) {
return (
<Card sx={sx} {...other}>
<CardHeader title={title} subheader={subheader} />
<Box
sx={{
p: 3,
gap: 2,
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
}}
>
{list.map((site) => (
<Box
key={site.label}
sx={(theme) => ({
py: 2.5,
display: 'flex',
borderRadius: 1.5,
textAlign: 'center',
alignItems: 'center',
flexDirection: 'column',
border: `solid 1px ${varAlpha(theme.vars.palette.grey['500Channel'], 0.12)}`,
})}
>
{site.value === 'twitter' && <Iconify width={32} icon="socials:twitter" />}
{site.value === 'facebook' && <Iconify width={32} icon="socials:facebook" />}
{site.value === 'google' && <Iconify width={32} icon="socials:google" />}
{site.value === 'linkedin' && <Iconify width={32} icon="socials:linkedin" />}
<Typography variant="h6" sx={{ mt: 1 }}>
{fShortenNumber(site.total)}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{site.label}
</Typography>
</Box>
))}
</Box>
</Card>
);
}

View File

@@ -0,0 +1,61 @@
import type { CardProps } from '@mui/material/Card';
import type { ChartOptions } from 'src/components/chart';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import { useTheme, alpha as hexAlpha } from '@mui/material/styles';
import { Chart, useChart } from 'src/components/chart';
// ----------------------------------------------------------------------
type Props = CardProps & {
title?: string;
subheader?: string;
chart: {
colors?: string[];
categories?: string[];
series: {
name: string;
data: number[];
}[];
options?: ChartOptions;
};
};
export function AnalyticsWebsiteVisits({ title, subheader, chart, sx, ...other }: Props) {
const theme = useTheme();
const chartColors = chart.colors ?? [
hexAlpha(theme.palette.primary.dark, 0.8),
hexAlpha(theme.palette.warning.main, 0.8),
];
const chartOptions = useChart({
colors: chartColors,
stroke: { width: 2, colors: ['transparent'] },
xaxis: { categories: chart.categories },
legend: { show: true },
tooltip: { y: { formatter: (value: number) => `${value} visits` } },
...chart.options,
});
return (
<Card sx={sx} {...other}>
<CardHeader title={title} subheader={subheader} />
<Chart
type="bar"
series={chart.series}
options={chartOptions}
slotProps={{ loading: { p: 2.5 } }}
sx={{
pl: 1,
py: 2.5,
pr: 2.5,
height: 364,
}}
/>
</Card>
);
}

View File

@@ -0,0 +1,142 @@
import type { CardProps } from '@mui/material/Card';
import type { PaletteColorKey } from 'src/theme/core';
import type { ChartOptions } from 'src/components/chart';
import { varAlpha } from 'minimal-shared/utils';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import { useTheme } from '@mui/material/styles';
import { fNumber, fPercent, fShortenNumber } from 'src/utils/format-number';
import { Iconify } from 'src/components/iconify';
import { SvgColor } from 'src/components/svg-color';
import { Chart, useChart } from 'src/components/chart';
// ----------------------------------------------------------------------
type Props = CardProps & {
title: string;
total: number;
percent: number;
color?: PaletteColorKey;
icon: React.ReactNode;
chart: {
series: number[];
categories: string[];
options?: ChartOptions;
};
};
export function AnalyticsWidgetSummary({
sx,
icon,
title,
total,
chart,
percent,
color = 'primary',
...other
}: Props) {
const theme = useTheme();
const chartColors = [theme.palette[color].dark];
const chartOptions = useChart({
chart: { sparkline: { enabled: true } },
colors: chartColors,
xaxis: { categories: chart.categories },
grid: {
padding: {
top: 6,
left: 6,
right: 6,
bottom: 6,
},
},
tooltip: {
y: { formatter: (value: number) => fNumber(value), title: { formatter: () => '' } },
},
markers: {
strokeWidth: 0,
},
...chart.options,
});
const renderTrending = () => (
<Box
sx={{
top: 16,
gap: 0.5,
right: 16,
display: 'flex',
position: 'absolute',
alignItems: 'center',
}}
>
<Iconify width={20} icon={percent < 0 ? 'eva:trending-down-fill' : 'eva:trending-up-fill'} />
<Box component="span" sx={{ typography: 'subtitle2' }}>
{percent > 0 && '+'}
{fPercent(percent)}
</Box>
</Box>
);
return (
<Card
sx={[
() => ({
p: 3,
boxShadow: 'none',
position: 'relative',
color: `${color}.darker`,
backgroundColor: 'common.white',
backgroundImage: `linear-gradient(135deg, ${varAlpha(theme.vars.palette[color].lighterChannel, 0.48)}, ${varAlpha(theme.vars.palette[color].lightChannel, 0.48)})`,
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
<Box sx={{ width: 48, height: 48, mb: 3 }}>{icon}</Box>
{renderTrending()}
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'flex-end',
justifyContent: 'flex-end',
}}
>
<Box sx={{ flexGrow: 1, minWidth: 112 }}>
<Box sx={{ mb: 1, typography: 'subtitle2' }}>{title}</Box>
<Box sx={{ typography: 'h4' }}>{fShortenNumber(total)}</Box>
</Box>
<Chart
type="line"
series={[{ data: chart.series }]}
options={chartOptions}
sx={{ width: 84, height: 56 }}
/>
</Box>
<SvgColor
src="/assets/background/shape-square.svg"
sx={{
top: 0,
left: -20,
width: 240,
zIndex: -1,
height: 240,
opacity: 0.24,
position: 'absolute',
color: `${color}.main`,
}}
/>
</Card>
);
}

View File

@@ -0,0 +1 @@
export * from './overview-analytics-view';

View File

@@ -0,0 +1,156 @@
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import { DashboardContent } from 'src/layouts/dashboard';
import { _posts, _tasks, _traffic, _timeline } from 'src/_mock';
import { AnalyticsNews } from '../analytics-news';
import { AnalyticsTasks } from '../analytics-tasks';
import { AnalyticsCurrentVisits } from '../analytics-current-visits';
import { AnalyticsOrderTimeline } from '../analytics-order-timeline';
import { AnalyticsWebsiteVisits } from '../analytics-website-visits';
import { AnalyticsWidgetSummary } from '../analytics-widget-summary';
import { AnalyticsTrafficBySite } from '../analytics-traffic-by-site';
import { AnalyticsCurrentSubject } from '../analytics-current-subject';
import { AnalyticsConversionRates } from '../analytics-conversion-rates';
// ----------------------------------------------------------------------
export function OverviewAnalyticsView() {
return (
<DashboardContent maxWidth="xl">
<Typography variant="h4" sx={{ mb: { xs: 3, md: 5 } }}>
Hi, Welcome back 👋
</Typography>
<Grid container spacing={3}>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<AnalyticsWidgetSummary
title="Weekly sales"
percent={2.6}
total={714000}
icon={<img alt="Weekly sales" src="/assets/icons/glass/ic-glass-bag.svg" />}
chart={{
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'],
series: [22, 8, 35, 50, 82, 84, 77, 12],
}}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<AnalyticsWidgetSummary
title="New users"
percent={-0.1}
total={1352831}
color="secondary"
icon={<img alt="New users" src="/assets/icons/glass/ic-glass-users.svg" />}
chart={{
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'],
series: [56, 47, 40, 62, 73, 30, 23, 54],
}}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<AnalyticsWidgetSummary
title="Purchase orders"
percent={2.8}
total={1723315}
color="warning"
icon={<img alt="Purchase orders" src="/assets/icons/glass/ic-glass-buy.svg" />}
chart={{
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'],
series: [40, 70, 50, 28, 70, 75, 7, 64],
}}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<AnalyticsWidgetSummary
title="Messages"
percent={3.6}
total={234}
color="error"
icon={<img alt="Messages" src="/assets/icons/glass/ic-glass-message.svg" />}
chart={{
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'],
series: [56, 30, 23, 54, 47, 40, 62, 73],
}}
/>
</Grid>
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
<AnalyticsCurrentVisits
title="Current visits"
chart={{
series: [
{ label: 'America', value: 3500 },
{ label: 'Asia', value: 2500 },
{ label: 'Europe', value: 1500 },
{ label: 'Africa', value: 500 },
],
}}
/>
</Grid>
<Grid size={{ xs: 12, md: 6, lg: 8 }}>
<AnalyticsWebsiteVisits
title="Website visits"
subheader="(+43%) than last year"
chart={{
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep'],
series: [
{ name: 'Team A', data: [43, 33, 22, 37, 67, 68, 37, 24, 55] },
{ name: 'Team B', data: [51, 70, 47, 67, 40, 37, 24, 70, 24] },
],
}}
/>
</Grid>
<Grid size={{ xs: 12, md: 6, lg: 8 }}>
<AnalyticsConversionRates
title="Conversion rates"
subheader="(+43%) than last year"
chart={{
categories: ['Italy', 'Japan', 'China', 'Canada', 'France'],
series: [
{ name: '2022', data: [44, 55, 41, 64, 22] },
{ name: '2023', data: [53, 32, 33, 52, 13] },
],
}}
/>
</Grid>
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
<AnalyticsCurrentSubject
title="Current subject"
chart={{
categories: ['English', 'History', 'Physics', 'Geography', 'Chinese', 'Math'],
series: [
{ name: 'Series 1', data: [80, 50, 30, 40, 100, 20] },
{ name: 'Series 2', data: [20, 30, 40, 80, 20, 80] },
{ name: 'Series 3', data: [44, 76, 78, 13, 43, 10] },
],
}}
/>
</Grid>
<Grid size={{ xs: 12, md: 6, lg: 8 }}>
<AnalyticsNews title="News" list={_posts.slice(0, 5)} />
</Grid>
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
<AnalyticsOrderTimeline title="Order timeline" list={_timeline} />
</Grid>
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
<AnalyticsTrafficBySite title="Traffic by site" list={_traffic} />
</Grid>
<Grid size={{ xs: 12, md: 6, lg: 8 }}>
<AnalyticsTasks title="Tasks" list={_tasks} />
</Grid>
</Grid>
</DashboardContent>
);
}

View File

@@ -0,0 +1,47 @@
import type { BoxProps } from '@mui/material/Box';
import Box from '@mui/material/Box';
import Badge from '@mui/material/Badge';
import { RouterLink } from 'src/routes/components';
import { Iconify } from 'src/components/iconify';
// ----------------------------------------------------------------------
type CartIconProps = BoxProps & {
totalItems: number;
};
export function CartIcon({ totalItems, sx, ...other }: CartIconProps) {
return (
<Box
component={RouterLink}
href="#"
sx={[
(theme) => ({
right: 0,
top: 112,
zIndex: 999,
display: 'flex',
cursor: 'pointer',
position: 'fixed',
color: 'text.primary',
borderTopLeftRadius: 16,
borderBottomLeftRadius: 16,
bgcolor: 'background.paper',
padding: theme.spacing(1, 3, 1, 2),
boxShadow: theme.vars.customShadows.dropdown,
transition: theme.transitions.create(['opacity']),
'&:hover': { opacity: 0.72 },
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
<Badge showZero badgeContent={totalItems} color="error" max={99}>
<Iconify icon="solar:cart-3-bold" width={24} />
</Badge>
</Box>
);
}

Some files were not shown because too many files have changed in this diff Show More