init project
Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled

This commit is contained in:
how2ice
2025-12-12 14:26:25 +09:00
commit 005cf56baf
43188 changed files with 1079531 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
import { CssVarsProvider } from '@mui/joy/styles';
import CssBaseline from '@mui/joy/CssBaseline';
import Box from '@mui/joy/Box';
import Sidebar from './components/Sidebar';
import Header from './components/Header';
import MyProfile from './components/MyProfile';
export default function JoyOrderDashboardTemplate() {
return (
<CssVarsProvider disableTransitionOnChange>
<CssBaseline />
<Box sx={{ display: 'flex', minHeight: '100dvh' }}>
<Sidebar />
<Header />
<Box
component="main"
className="MainContent"
sx={{
pt: { xs: 'calc(12px + var(--Header-height))', md: 3 },
pb: { xs: 2, sm: 2, md: 3 },
flex: 1,
display: 'flex',
flexDirection: 'column',
minWidth: 0,
height: '100dvh',
gap: 1,
overflow: 'auto',
}}
>
<MyProfile />
</Box>
</Box>
</CssVarsProvider>
);
}

View File

@@ -0,0 +1,56 @@
import * as React from 'react';
import { useColorScheme } from '@mui/joy/styles';
import IconButton, { IconButtonProps } from '@mui/joy/IconButton';
import DarkModeRoundedIcon from '@mui/icons-material/DarkModeRounded';
import LightModeIcon from '@mui/icons-material/LightMode';
export default function ColorSchemeToggle(props: IconButtonProps) {
const { onClick, sx, ...other } = props;
const { mode, setMode } = useColorScheme();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<IconButton
size="sm"
variant="outlined"
color="neutral"
{...other}
sx={sx}
disabled
/>
);
}
return (
<IconButton
data-screenshot="toggle-mode"
size="sm"
variant="outlined"
color="neutral"
{...props}
onClick={(event) => {
if (mode === 'light') {
setMode('dark');
} else {
setMode('light');
}
onClick?.(event);
}}
sx={[
mode === 'dark'
? { '& > *:first-of-type': { display: 'none' } }
: { '& > *:first-of-type': { display: 'initial' } },
mode === 'light'
? { '& > *:last-of-type': { display: 'none' } }
: { '& > *:last-of-type': { display: 'initial' } },
...(Array.isArray(sx) ? sx : [sx]),
]}
>
<DarkModeRoundedIcon />
<LightModeIcon />
</IconButton>
);
}

View File

@@ -0,0 +1,483 @@
import Autocomplete from '@mui/joy/Autocomplete';
import AutocompleteOption from '@mui/joy/AutocompleteOption';
import AspectRatio from '@mui/joy/AspectRatio';
import FormControl, { FormControlProps } from '@mui/joy/FormControl';
import FormLabel from '@mui/joy/FormLabel';
import ListItemDecorator from '@mui/joy/ListItemDecorator';
import Typography from '@mui/joy/Typography';
export default function ContrySelector(props: FormControlProps) {
const { sx, ...other } = props;
return (
<FormControl
{...other}
sx={[{ display: { sm: 'contents' } }, ...(Array.isArray(sx) ? sx : [sx])]}
>
<FormLabel>Country</FormLabel>
<Autocomplete
size="sm"
autoHighlight
isOptionEqualToValue={(option, value) => option.code === value.code}
defaultValue={{ code: 'TH', label: 'Thailand', phone: '66' }}
options={countries}
renderOption={(optionProps, option) => (
<AutocompleteOption {...optionProps}>
<ListItemDecorator>
<AspectRatio ratio="1" sx={{ minWidth: 20, borderRadius: '50%' }}>
<img
loading="lazy"
width="20"
srcSet={`https://flagcdn.com/w40/${option.code.toLowerCase()}.png 2x`}
src={`https://flagcdn.com/w20/${option.code.toLowerCase()}.png`}
alt=""
/>
</AspectRatio>
</ListItemDecorator>
{option.label}
<Typography component="span" textColor="text.tertiary" sx={{ ml: 0.5 }}>
(+{option.phone})
</Typography>
</AutocompleteOption>
)}
slotProps={{
input: {
autoComplete: 'new-password', // disable autocomplete and autofill
},
}}
/>
</FormControl>
);
}
interface CountryType {
code: string;
label: string;
phone: string;
suggested?: boolean;
}
// From https://bitbucket.org/atlassian/atlaskit-mk-2/raw/4ad0e56649c3e6c973e226b7efaeb28cb240ccb0/packages/core/select/src/data/countries.js
const countries: readonly CountryType[] = [
{ code: 'AD', label: 'Andorra', phone: '376' },
{
code: 'AE',
label: 'United Arab Emirates',
phone: '971',
},
{ code: 'AF', label: 'Afghanistan', phone: '93' },
{
code: 'AG',
label: 'Antigua and Barbuda',
phone: '1-268',
},
{ code: 'AI', label: 'Anguilla', phone: '1-264' },
{ code: 'AL', label: 'Albania', phone: '355' },
{ code: 'AM', label: 'Armenia', phone: '374' },
{ code: 'AO', label: 'Angola', phone: '244' },
{ code: 'AQ', label: 'Antarctica', phone: '672' },
{ code: 'AR', label: 'Argentina', phone: '54' },
{ code: 'AS', label: 'American Samoa', phone: '1-684' },
{ code: 'AT', label: 'Austria', phone: '43' },
{
code: 'AU',
label: 'Australia',
phone: '61',
suggested: true,
},
{ code: 'AW', label: 'Aruba', phone: '297' },
{ code: 'AX', label: 'Alland Islands', phone: '358' },
{ code: 'AZ', label: 'Azerbaijan', phone: '994' },
{
code: 'BA',
label: 'Bosnia and Herzegovina',
phone: '387',
},
{ code: 'BB', label: 'Barbados', phone: '1-246' },
{ code: 'BD', label: 'Bangladesh', phone: '880' },
{ code: 'BE', label: 'Belgium', phone: '32' },
{ code: 'BF', label: 'Burkina Faso', phone: '226' },
{ code: 'BG', label: 'Bulgaria', phone: '359' },
{ code: 'BH', label: 'Bahrain', phone: '973' },
{ code: 'BI', label: 'Burundi', phone: '257' },
{ code: 'BJ', label: 'Benin', phone: '229' },
{ code: 'BL', label: 'Saint Barthelemy', phone: '590' },
{ code: 'BM', label: 'Bermuda', phone: '1-441' },
{ code: 'BN', label: 'Brunei Darussalam', phone: '673' },
{ code: 'BO', label: 'Bolivia', phone: '591' },
{ code: 'BR', label: 'Brazil', phone: '55' },
{ code: 'BS', label: 'Bahamas', phone: '1-242' },
{ code: 'BT', label: 'Bhutan', phone: '975' },
{ code: 'BV', label: 'Bouvet Island', phone: '47' },
{ code: 'BW', label: 'Botswana', phone: '267' },
{ code: 'BY', label: 'Belarus', phone: '375' },
{ code: 'BZ', label: 'Belize', phone: '501' },
{
code: 'CA',
label: 'Canada',
phone: '1',
suggested: true,
},
{
code: 'CC',
label: 'Cocos (Keeling) Islands',
phone: '61',
},
{
code: 'CD',
label: 'Congo, Democratic Republic of the',
phone: '243',
},
{
code: 'CF',
label: 'Central African Republic',
phone: '236',
},
{
code: 'CG',
label: 'Congo, Republic of the',
phone: '242',
},
{ code: 'CH', label: 'Switzerland', phone: '41' },
{ code: 'CI', label: "Cote d'Ivoire", phone: '225' },
{ code: 'CK', label: 'Cook Islands', phone: '682' },
{ code: 'CL', label: 'Chile', phone: '56' },
{ code: 'CM', label: 'Cameroon', phone: '237' },
{ code: 'CN', label: 'China', phone: '86' },
{ code: 'CO', label: 'Colombia', phone: '57' },
{ code: 'CR', label: 'Costa Rica', phone: '506' },
{ code: 'CU', label: 'Cuba', phone: '53' },
{ code: 'CV', label: 'Cape Verde', phone: '238' },
{ code: 'CW', label: 'Curacao', phone: '599' },
{ code: 'CX', label: 'Christmas Island', phone: '61' },
{ code: 'CY', label: 'Cyprus', phone: '357' },
{ code: 'CZ', label: 'Czech Republic', phone: '420' },
{
code: 'DE',
label: 'Germany',
phone: '49',
suggested: true,
},
{ code: 'DJ', label: 'Djibouti', phone: '253' },
{ code: 'DK', label: 'Denmark', phone: '45' },
{ code: 'DM', label: 'Dominica', phone: '1-767' },
{
code: 'DO',
label: 'Dominican Republic',
phone: '1-809',
},
{ code: 'DZ', label: 'Algeria', phone: '213' },
{ code: 'EC', label: 'Ecuador', phone: '593' },
{ code: 'EE', label: 'Estonia', phone: '372' },
{ code: 'EG', label: 'Egypt', phone: '20' },
{ code: 'EH', label: 'Western Sahara', phone: '212' },
{ code: 'ER', label: 'Eritrea', phone: '291' },
{ code: 'ES', label: 'Spain', phone: '34' },
{ code: 'ET', label: 'Ethiopia', phone: '251' },
{ code: 'FI', label: 'Finland', phone: '358' },
{ code: 'FJ', label: 'Fiji', phone: '679' },
{
code: 'FK',
label: 'Falkland Islands (Malvinas)',
phone: '500',
},
{
code: 'FM',
label: 'Micronesia, Federated States of',
phone: '691',
},
{ code: 'FO', label: 'Faroe Islands', phone: '298' },
{
code: 'FR',
label: 'France',
phone: '33',
suggested: true,
},
{ code: 'GA', label: 'Gabon', phone: '241' },
{ code: 'GB', label: 'United Kingdom', phone: '44' },
{ code: 'GD', label: 'Grenada', phone: '1-473' },
{ code: 'GE', label: 'Georgia', phone: '995' },
{ code: 'GF', label: 'French Guiana', phone: '594' },
{ code: 'GG', label: 'Guernsey', phone: '44' },
{ code: 'GH', label: 'Ghana', phone: '233' },
{ code: 'GI', label: 'Gibraltar', phone: '350' },
{ code: 'GL', label: 'Greenland', phone: '299' },
{ code: 'GM', label: 'Gambia', phone: '220' },
{ code: 'GN', label: 'Guinea', phone: '224' },
{ code: 'GP', label: 'Guadeloupe', phone: '590' },
{ code: 'GQ', label: 'Equatorial Guinea', phone: '240' },
{ code: 'GR', label: 'Greece', phone: '30' },
{
code: 'GS',
label: 'South Georgia and the South Sandwich Islands',
phone: '500',
},
{ code: 'GT', label: 'Guatemala', phone: '502' },
{ code: 'GU', label: 'Guam', phone: '1-671' },
{ code: 'GW', label: 'Guinea-Bissau', phone: '245' },
{ code: 'GY', label: 'Guyana', phone: '592' },
{ code: 'HK', label: 'Hong Kong', phone: '852' },
{
code: 'HM',
label: 'Heard Island and McDonald Islands',
phone: '672',
},
{ code: 'HN', label: 'Honduras', phone: '504' },
{ code: 'HR', label: 'Croatia', phone: '385' },
{ code: 'HT', label: 'Haiti', phone: '509' },
{ code: 'HU', label: 'Hungary', phone: '36' },
{ code: 'ID', label: 'Indonesia', phone: '62' },
{ code: 'IE', label: 'Ireland', phone: '353' },
{ code: 'IL', label: 'Israel', phone: '972' },
{ code: 'IM', label: 'Isle of Man', phone: '44' },
{ code: 'IN', label: 'India', phone: '91' },
{
code: 'IO',
label: 'British Indian Ocean Territory',
phone: '246',
},
{ code: 'IQ', label: 'Iraq', phone: '964' },
{
code: 'IR',
label: 'Iran, Islamic Republic of',
phone: '98',
},
{ code: 'IS', label: 'Iceland', phone: '354' },
{ code: 'IT', label: 'Italy', phone: '39' },
{ code: 'JE', label: 'Jersey', phone: '44' },
{ code: 'JM', label: 'Jamaica', phone: '1-876' },
{ code: 'JO', label: 'Jordan', phone: '962' },
{
code: 'JP',
label: 'Japan',
phone: '81',
suggested: true,
},
{ code: 'KE', label: 'Kenya', phone: '254' },
{ code: 'KG', label: 'Kyrgyzstan', phone: '996' },
{ code: 'KH', label: 'Cambodia', phone: '855' },
{ code: 'KI', label: 'Kiribati', phone: '686' },
{ code: 'KM', label: 'Comoros', phone: '269' },
{
code: 'KN',
label: 'Saint Kitts and Nevis',
phone: '1-869',
},
{
code: 'KP',
label: "Korea, Democratic People's Republic of",
phone: '850',
},
{ code: 'KR', label: 'Korea, Republic of', phone: '82' },
{ code: 'KW', label: 'Kuwait', phone: '965' },
{ code: 'KY', label: 'Cayman Islands', phone: '1-345' },
{ code: 'KZ', label: 'Kazakhstan', phone: '7' },
{
code: 'LA',
label: "Lao People's Democratic Republic",
phone: '856',
},
{ code: 'LB', label: 'Lebanon', phone: '961' },
{ code: 'LC', label: 'Saint Lucia', phone: '1-758' },
{ code: 'LI', label: 'Liechtenstein', phone: '423' },
{ code: 'LK', label: 'Sri Lanka', phone: '94' },
{ code: 'LR', label: 'Liberia', phone: '231' },
{ code: 'LS', label: 'Lesotho', phone: '266' },
{ code: 'LT', label: 'Lithuania', phone: '370' },
{ code: 'LU', label: 'Luxembourg', phone: '352' },
{ code: 'LV', label: 'Latvia', phone: '371' },
{ code: 'LY', label: 'Libya', phone: '218' },
{ code: 'MA', label: 'Morocco', phone: '212' },
{ code: 'MC', label: 'Monaco', phone: '377' },
{
code: 'MD',
label: 'Moldova, Republic of',
phone: '373',
},
{ code: 'ME', label: 'Montenegro', phone: '382' },
{
code: 'MF',
label: 'Saint Martin (French part)',
phone: '590',
},
{ code: 'MG', label: 'Madagascar', phone: '261' },
{ code: 'MH', label: 'Marshall Islands', phone: '692' },
{
code: 'MK',
label: 'Macedonia, the Former Yugoslav Republic of',
phone: '389',
},
{ code: 'ML', label: 'Mali', phone: '223' },
{ code: 'MM', label: 'Myanmar', phone: '95' },
{ code: 'MN', label: 'Mongolia', phone: '976' },
{ code: 'MO', label: 'Macao', phone: '853' },
{
code: 'MP',
label: 'Northern Mariana Islands',
phone: '1-670',
},
{ code: 'MQ', label: 'Martinique', phone: '596' },
{ code: 'MR', label: 'Mauritania', phone: '222' },
{ code: 'MS', label: 'Montserrat', phone: '1-664' },
{ code: 'MT', label: 'Malta', phone: '356' },
{ code: 'MU', label: 'Mauritius', phone: '230' },
{ code: 'MV', label: 'Maldives', phone: '960' },
{ code: 'MW', label: 'Malawi', phone: '265' },
{ code: 'MX', label: 'Mexico', phone: '52' },
{ code: 'MY', label: 'Malaysia', phone: '60' },
{ code: 'MZ', label: 'Mozambique', phone: '258' },
{ code: 'NA', label: 'Namibia', phone: '264' },
{ code: 'NC', label: 'New Caledonia', phone: '687' },
{ code: 'NE', label: 'Niger', phone: '227' },
{ code: 'NF', label: 'Norfolk Island', phone: '672' },
{ code: 'NG', label: 'Nigeria', phone: '234' },
{ code: 'NI', label: 'Nicaragua', phone: '505' },
{ code: 'NL', label: 'Netherlands', phone: '31' },
{ code: 'NO', label: 'Norway', phone: '47' },
{ code: 'NP', label: 'Nepal', phone: '977' },
{ code: 'NR', label: 'Nauru', phone: '674' },
{ code: 'NU', label: 'Niue', phone: '683' },
{ code: 'NZ', label: 'New Zealand', phone: '64' },
{ code: 'OM', label: 'Oman', phone: '968' },
{ code: 'PA', label: 'Panama', phone: '507' },
{ code: 'PE', label: 'Peru', phone: '51' },
{ code: 'PF', label: 'French Polynesia', phone: '689' },
{ code: 'PG', label: 'Papua New Guinea', phone: '675' },
{ code: 'PH', label: 'Philippines', phone: '63' },
{ code: 'PK', label: 'Pakistan', phone: '92' },
{ code: 'PL', label: 'Poland', phone: '48' },
{
code: 'PM',
label: 'Saint Pierre and Miquelon',
phone: '508',
},
{ code: 'PN', label: 'Pitcairn', phone: '870' },
{ code: 'PR', label: 'Puerto Rico', phone: '1' },
{
code: 'PS',
label: 'Palestine, State of',
phone: '970',
},
{ code: 'PT', label: 'Portugal', phone: '351' },
{ code: 'PW', label: 'Palau', phone: '680' },
{ code: 'PY', label: 'Paraguay', phone: '595' },
{ code: 'QA', label: 'Qatar', phone: '974' },
{ code: 'RE', label: 'Reunion', phone: '262' },
{ code: 'RO', label: 'Romania', phone: '40' },
{ code: 'RS', label: 'Serbia', phone: '381' },
{ code: 'RU', label: 'Russian Federation', phone: '7' },
{ code: 'RW', label: 'Rwanda', phone: '250' },
{ code: 'SA', label: 'Saudi Arabia', phone: '966' },
{ code: 'SB', label: 'Solomon Islands', phone: '677' },
{ code: 'SC', label: 'Seychelles', phone: '248' },
{ code: 'SD', label: 'Sudan', phone: '249' },
{ code: 'SE', label: 'Sweden', phone: '46' },
{ code: 'SG', label: 'Singapore', phone: '65' },
{ code: 'SH', label: 'Saint Helena', phone: '290' },
{ code: 'SI', label: 'Slovenia', phone: '386' },
{
code: 'SJ',
label: 'Svalbard and Jan Mayen',
phone: '47',
},
{ code: 'SK', label: 'Slovakia', phone: '421' },
{ code: 'SL', label: 'Sierra Leone', phone: '232' },
{ code: 'SM', label: 'San Marino', phone: '378' },
{ code: 'SN', label: 'Senegal', phone: '221' },
{ code: 'SO', label: 'Somalia', phone: '252' },
{ code: 'SR', label: 'Suriname', phone: '597' },
{ code: 'SS', label: 'South Sudan', phone: '211' },
{
code: 'ST',
label: 'Sao Tome and Principe',
phone: '239',
},
{ code: 'SV', label: 'El Salvador', phone: '503' },
{
code: 'SX',
label: 'Sint Maarten (Dutch part)',
phone: '1-721',
},
{
code: 'SY',
label: 'Syrian Arab Republic',
phone: '963',
},
{ code: 'SZ', label: 'Swaziland', phone: '268' },
{
code: 'TC',
label: 'Turks and Caicos Islands',
phone: '1-649',
},
{ code: 'TD', label: 'Chad', phone: '235' },
{
code: 'TF',
label: 'French Southern Territories',
phone: '262',
},
{ code: 'TG', label: 'Togo', phone: '228' },
{ code: 'TH', label: 'Thailand', phone: '66' },
{ code: 'TJ', label: 'Tajikistan', phone: '992' },
{ code: 'TK', label: 'Tokelau', phone: '690' },
{ code: 'TL', label: 'Timor-Leste', phone: '670' },
{ code: 'TM', label: 'Turkmenistan', phone: '993' },
{ code: 'TN', label: 'Tunisia', phone: '216' },
{ code: 'TO', label: 'Tonga', phone: '676' },
{ code: 'TR', label: 'Turkey', phone: '90' },
{
code: 'TT',
label: 'Trinidad and Tobago',
phone: '1-868',
},
{ code: 'TV', label: 'Tuvalu', phone: '688' },
{
code: 'TW',
label: 'Taiwan',
phone: '886',
},
{
code: 'TZ',
label: 'United Republic of Tanzania',
phone: '255',
},
{ code: 'UA', label: 'Ukraine', phone: '380' },
{ code: 'UG', label: 'Uganda', phone: '256' },
{
code: 'US',
label: 'United States',
phone: '1',
suggested: true,
},
{ code: 'UY', label: 'Uruguay', phone: '598' },
{ code: 'UZ', label: 'Uzbekistan', phone: '998' },
{
code: 'VA',
label: 'Holy See (Vatican City State)',
phone: '379',
},
{
code: 'VC',
label: 'Saint Vincent and the Grenadines',
phone: '1-784',
},
{ code: 'VE', label: 'Venezuela', phone: '58' },
{
code: 'VG',
label: 'British Virgin Islands',
phone: '1-284',
},
{
code: 'VI',
label: 'US Virgin Islands',
phone: '1-340',
},
{ code: 'VN', label: 'Vietnam', phone: '84' },
{ code: 'VU', label: 'Vanuatu', phone: '678' },
{ code: 'WF', label: 'Wallis and Futuna', phone: '681' },
{ code: 'WS', label: 'Samoa', phone: '685' },
{ code: 'XK', label: 'Kosovo', phone: '383' },
{ code: 'YE', label: 'Yemen', phone: '967' },
{ code: 'YT', label: 'Mayotte', phone: '262' },
{ code: 'ZA', label: 'South Africa', phone: '27' },
{ code: 'ZM', label: 'Zambia', phone: '260' },
{ code: 'ZW', label: 'Zimbabwe', phone: '263' },
];

View File

@@ -0,0 +1,48 @@
import * as React from 'react';
import Card, { CardProps } from '@mui/joy/Card';
import Link from '@mui/joy/Link';
import Typography from '@mui/joy/Typography';
import AspectRatio from '@mui/joy/AspectRatio';
import FileUploadRoundedIcon from '@mui/icons-material/FileUploadRounded';
export default function DropZone(
props: CardProps & { icon?: React.ReactElement<any> },
) {
const { icon, sx, ...other } = props;
return (
<Card
variant="soft"
{...other}
sx={[
{
borderRadius: 'sm',
display: 'flex',
flexDirection: 'column',
gap: 1,
alignItems: 'center',
px: 3,
flexGrow: 1,
boxShadow: 'none',
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
<AspectRatio
ratio="1"
variant="solid"
color="primary"
sx={{ minWidth: 32, borderRadius: '50%', '--Icon-fontSize': '16px' }}
>
<div>{icon ?? <FileUploadRoundedIcon />}</div>
</AspectRatio>
<Typography level="body-sm" sx={{ textAlign: 'center' }}>
<Link component="button" overlay>
Click to upload
</Link>{' '}
or drag and drop
<br /> SVG, PNG, JPG or GIF (max. 800x400px)
</Typography>
</Card>
);
}

View File

@@ -0,0 +1,41 @@
import Box, { BoxProps } from '@mui/joy/Box';
import Select from '@mui/joy/Select';
import Option from '@mui/joy/Option';
import IconButton from '@mui/joy/IconButton';
import FormatBoldRoundedIcon from '@mui/icons-material/FormatBoldRounded';
import FormatItalicRoundedIcon from '@mui/icons-material/FormatItalicRounded';
import StrikethroughSRoundedIcon from '@mui/icons-material/StrikethroughSRounded';
import FormatListBulletedRoundedIcon from '@mui/icons-material/FormatListBulletedRounded';
export default function EditorToolbar(props: BoxProps) {
const { sx, ...other } = props;
return (
<Box
{...other}
sx={[
{ display: 'flex', gap: 0.5, '& > button': { '--Icon-fontSize': '16px' } },
...(Array.isArray(sx) ? sx : [sx]),
]}
>
<Select size="sm" defaultValue="1" sx={{ minWidth: 160 }}>
<Option value="1">Normal text</Option>
<Option value="2" sx={{ fontFamily: 'code' }}>
Code text
</Option>
</Select>
<IconButton size="sm" variant="plain" color="neutral">
<FormatBoldRoundedIcon />
</IconButton>
<IconButton size="sm" variant="plain" color="neutral">
<FormatItalicRoundedIcon />
</IconButton>
<IconButton size="sm" variant="plain" color="neutral">
<StrikethroughSRoundedIcon />
</IconButton>
<IconButton size="sm" variant="plain" color="neutral">
<FormatListBulletedRoundedIcon />
</IconButton>
</Box>
);
}

View File

@@ -0,0 +1,79 @@
import * as React from 'react';
import AspectRatio from '@mui/joy/AspectRatio';
import Box from '@mui/joy/Box';
import Card, { CardProps } from '@mui/joy/Card';
import CardContent from '@mui/joy/CardContent';
import IconButton from '@mui/joy/IconButton';
import LinearProgress from '@mui/joy/LinearProgress';
import Typography from '@mui/joy/Typography';
import InsertDriveFileRoundedIcon from '@mui/icons-material/InsertDriveFileRounded';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import RemoveCircleOutlineRoundedIcon from '@mui/icons-material/RemoveCircleOutlineRounded';
export default function FileUpload(
props: CardProps & {
icon?: React.ReactElement<any>;
fileName: string;
fileSize: string;
progress: number;
},
) {
const { icon, fileName, fileSize, progress, sx, ...other } = props;
return (
<Card
variant="outlined"
orientation="horizontal"
{...other}
sx={[
{
gap: 1.5,
alignItems: 'flex-start',
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
<AspectRatio
ratio="1"
variant="soft"
color="neutral"
sx={{ minWidth: 32, borderRadius: '50%', '--Icon-fontSize': '16px' }}
>
<div>{icon ?? <InsertDriveFileRoundedIcon />}</div>
</AspectRatio>
<CardContent>
<Typography sx={{ fontSize: 'sm' }}>{fileName}</Typography>
<Typography level="body-xs">{fileSize}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<LinearProgress
color="neutral"
value={progress}
determinate
sx={[
progress >= 100 && {
color: 'var(--joy-palette-success-solidBg)',
},
]}
/>
<Typography sx={{ fontSize: 'xs' }}>{progress}%</Typography>
</Box>
</CardContent>
{progress >= 100 ? (
<AspectRatio
ratio="1"
variant="solid"
color="success"
sx={{ minWidth: 20, borderRadius: '50%', '--Icon-fontSize': '14px' }}
>
<div>
<CheckRoundedIcon />
</div>
</AspectRatio>
) : (
<IconButton variant="plain" color="danger" size="sm" sx={{ mt: -1, mr: -1 }}>
<RemoveCircleOutlineRoundedIcon />
</IconButton>
)}
</Card>
);
}

View File

@@ -0,0 +1,46 @@
import GlobalStyles from '@mui/joy/GlobalStyles';
import IconButton from '@mui/joy/IconButton';
import Sheet from '@mui/joy/Sheet';
import MenuRoundedIcon from '@mui/icons-material/MenuRounded';
import { toggleSidebar } from '../utils';
export default function Header() {
return (
<Sheet
sx={{
display: { xs: 'flex', md: 'none' },
alignItems: 'center',
justifyContent: 'space-between',
position: 'fixed',
top: 0,
width: '100vw',
height: 'var(--Header-height)',
zIndex: 9998,
p: 2,
gap: 1,
borderBottom: '1px solid',
borderColor: 'background.level1',
boxShadow: 'sm',
}}
>
<GlobalStyles
styles={(theme) => ({
':root': {
'--Header-height': '52px',
[theme.breakpoints.up('md')]: {
'--Header-height': '0px',
},
},
})}
/>
<IconButton
onClick={() => toggleSidebar()}
variant="outlined"
color="neutral"
size="sm"
>
<MenuRoundedIcon />
</IconButton>
</Sheet>
);
}

View File

@@ -0,0 +1,34 @@
import AspectRatio, { AspectRatioProps } from '@mui/joy/AspectRatio';
export default function MuiLogo(props: AspectRatioProps) {
const { sx, ...other } = props;
return (
<AspectRatio
ratio="1"
variant="plain"
{...other}
sx={[
{
width: 36,
borderRadius: 'sm',
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="20"
viewBox="0 0 36 32"
fill="none"
>
<path
d="M30.343 21.976a1 1 0 00.502-.864l.018-5.787a1 1 0 01.502-.864l3.137-1.802a1 1 0 011.498.867v10.521a1 1 0 01-.502.867l-11.839 6.8a1 1 0 01-.994.001l-9.291-5.314a1 1 0 01-.504-.868v-5.305c0-.006.007-.01.013-.007.005.003.012 0 .012-.007v-.006c0-.004.002-.008.006-.01l7.652-4.396c.007-.004.004-.015-.004-.015a.008.008 0 01-.008-.008l.015-5.201a1 1 0 00-1.5-.87l-5.687 3.277a1 1 0 01-.998 0L6.666 9.7a1 1 0 00-1.499.866v9.4a1 1 0 01-1.496.869l-3.166-1.81a1 1 0 01-.504-.87l.028-16.43A1 1 0 011.527.86l10.845 6.229a1 1 0 00.996 0L24.21.86a1 1 0 011.498.868v16.434a1 1 0 01-.501.867l-5.678 3.27a1 1 0 00.004 1.735l3.132 1.783a1 1 0 00.993-.002l6.685-3.839zM31 7.234a1 1 0 001.514.857l3-1.8A1 1 0 0036 5.434V1.766A1 1 0 0034.486.91l-3 1.8a1 1 0 00-.486.857v3.668z"
fill="#007FFF"
/>
</svg>
</div>
</AspectRatio>
);
}

View File

@@ -0,0 +1,399 @@
import AspectRatio from '@mui/joy/AspectRatio';
import Box from '@mui/joy/Box';
import Button from '@mui/joy/Button';
import Divider from '@mui/joy/Divider';
import FormControl from '@mui/joy/FormControl';
import FormLabel from '@mui/joy/FormLabel';
import FormHelperText from '@mui/joy/FormHelperText';
import Input from '@mui/joy/Input';
import IconButton from '@mui/joy/IconButton';
import Textarea from '@mui/joy/Textarea';
import Stack from '@mui/joy/Stack';
import Select from '@mui/joy/Select';
import Option from '@mui/joy/Option';
import Typography from '@mui/joy/Typography';
import Tabs from '@mui/joy/Tabs';
import TabList from '@mui/joy/TabList';
import Tab, { tabClasses } from '@mui/joy/Tab';
import Breadcrumbs from '@mui/joy/Breadcrumbs';
import Link from '@mui/joy/Link';
import Card from '@mui/joy/Card';
import CardActions from '@mui/joy/CardActions';
import CardOverflow from '@mui/joy/CardOverflow';
import HomeRoundedIcon from '@mui/icons-material/HomeRounded';
import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';
import EmailRoundedIcon from '@mui/icons-material/EmailRounded';
import AccessTimeFilledRoundedIcon from '@mui/icons-material/AccessTimeFilledRounded';
import VideocamRoundedIcon from '@mui/icons-material/VideocamRounded';
import InsertDriveFileRoundedIcon from '@mui/icons-material/InsertDriveFileRounded';
import EditRoundedIcon from '@mui/icons-material/EditRounded';
import DropZone from './DropZone';
import FileUpload from './FileUpload';
import CountrySelector from './CountrySelector';
import EditorToolbar from './EditorToolbar';
export default function MyProfile() {
return (
<Box sx={{ flex: 1, width: '100%' }}>
<Box
sx={{
position: 'sticky',
top: { sm: -100, md: -110 },
bgcolor: 'background.body',
zIndex: 9995,
}}
>
<Box sx={{ px: { xs: 2, md: 6 } }}>
<Breadcrumbs
size="sm"
aria-label="breadcrumbs"
separator={<ChevronRightRoundedIcon fontSize="sm" />}
sx={{ pl: 0 }}
>
<Link
underline="none"
color="neutral"
href="#some-link"
aria-label="Home"
>
<HomeRoundedIcon />
</Link>
<Link
underline="hover"
color="neutral"
href="#some-link"
sx={{ fontSize: 12, fontWeight: 500 }}
>
Users
</Link>
<Typography color="primary" sx={{ fontWeight: 500, fontSize: 12 }}>
My profile
</Typography>
</Breadcrumbs>
<Typography level="h2" component="h1" sx={{ mt: 1, mb: 2 }}>
My profile
</Typography>
</Box>
<Tabs defaultValue={0} sx={{ bgcolor: 'transparent' }}>
<TabList
tabFlex={1}
size="sm"
sx={{
pl: { xs: 0, md: 4 },
justifyContent: 'left',
[`&& .${tabClasses.root}`]: {
fontWeight: '600',
flex: 'initial',
color: 'text.tertiary',
[`&.${tabClasses.selected}`]: {
bgcolor: 'transparent',
color: 'text.primary',
'&::after': {
height: '2px',
bgcolor: 'primary.500',
},
},
},
}}
>
<Tab sx={{ borderRadius: '6px 6px 0 0' }} indicatorInset value={0}>
Settings
</Tab>
<Tab sx={{ borderRadius: '6px 6px 0 0' }} indicatorInset value={1}>
Team
</Tab>
<Tab sx={{ borderRadius: '6px 6px 0 0' }} indicatorInset value={2}>
Plan
</Tab>
<Tab sx={{ borderRadius: '6px 6px 0 0' }} indicatorInset value={3}>
Billing
</Tab>
</TabList>
</Tabs>
</Box>
<Stack
spacing={4}
sx={{
display: 'flex',
maxWidth: '800px',
mx: 'auto',
px: { xs: 2, md: 6 },
py: { xs: 2, md: 3 },
}}
>
<Card>
<Box sx={{ mb: 1 }}>
<Typography level="title-md">Personal info</Typography>
<Typography level="body-sm">
Customize how your profile information will apper to the networks.
</Typography>
</Box>
<Divider />
<Stack
direction="row"
spacing={3}
sx={{ display: { xs: 'none', md: 'flex' }, my: 1 }}
>
<Stack direction="column" spacing={1}>
<AspectRatio
ratio="1"
maxHeight={200}
sx={{ flex: 1, minWidth: 120, borderRadius: '100%' }}
>
<img
src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=286"
srcSet="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=286&dpr=2 2x"
loading="lazy"
alt=""
/>
</AspectRatio>
<IconButton
aria-label="upload new picture"
size="sm"
variant="outlined"
color="neutral"
sx={{
bgcolor: 'background.body',
position: 'absolute',
zIndex: 2,
borderRadius: '50%',
left: 100,
top: 170,
boxShadow: 'sm',
}}
>
<EditRoundedIcon />
</IconButton>
</Stack>
<Stack spacing={2} sx={{ flexGrow: 1 }}>
<Stack spacing={1}>
<FormLabel>Name</FormLabel>
<FormControl
sx={{ display: { sm: 'flex-column', md: 'flex-row' }, gap: 2 }}
>
<Input size="sm" placeholder="First name" />
<Input size="sm" placeholder="Last name" sx={{ flexGrow: 1 }} />
</FormControl>
</Stack>
<Stack direction="row" spacing={2}>
<FormControl>
<FormLabel>Role</FormLabel>
<Input size="sm" defaultValue="UI Developer" />
</FormControl>
<FormControl sx={{ flexGrow: 1 }}>
<FormLabel>Email</FormLabel>
<Input
size="sm"
type="email"
startDecorator={<EmailRoundedIcon />}
placeholder="email"
defaultValue="siriwatk@test.com"
sx={{ flexGrow: 1 }}
/>
</FormControl>
</Stack>
<div>
<CountrySelector />
</div>
<div>
<FormControl sx={{ display: { sm: 'contents' } }}>
<FormLabel>Timezone</FormLabel>
<Select
size="sm"
startDecorator={<AccessTimeFilledRoundedIcon />}
defaultValue="1"
>
<Option value="1">
Indochina Time (Bangkok){' '}
<Typography textColor="text.tertiary" sx={{ ml: 0.5 }}>
GMT+07:00
</Typography>
</Option>
<Option value="2">
Indochina Time (Ho Chi Minh City){' '}
<Typography textColor="text.tertiary" sx={{ ml: 0.5 }}>
GMT+07:00
</Typography>
</Option>
</Select>
</FormControl>
</div>
</Stack>
</Stack>
<Stack
direction="column"
spacing={2}
sx={{ display: { xs: 'flex', md: 'none' }, my: 1 }}
>
<Stack direction="row" spacing={2}>
<Stack direction="column" spacing={1}>
<AspectRatio
ratio="1"
maxHeight={108}
sx={{ flex: 1, minWidth: 108, borderRadius: '100%' }}
>
<img
src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=286"
srcSet="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=286&dpr=2 2x"
loading="lazy"
alt=""
/>
</AspectRatio>
<IconButton
aria-label="upload new picture"
size="sm"
variant="outlined"
color="neutral"
sx={{
bgcolor: 'background.body',
position: 'absolute',
zIndex: 2,
borderRadius: '50%',
left: 85,
top: 180,
boxShadow: 'sm',
}}
>
<EditRoundedIcon />
</IconButton>
</Stack>
<Stack spacing={1} sx={{ flexGrow: 1 }}>
<FormLabel>Name</FormLabel>
<FormControl
sx={{
display: {
sm: 'flex-column',
md: 'flex-row',
},
gap: 2,
}}
>
<Input size="sm" placeholder="First name" />
<Input size="sm" placeholder="Last name" />
</FormControl>
</Stack>
</Stack>
<FormControl>
<FormLabel>Role</FormLabel>
<Input size="sm" defaultValue="UI Developer" />
</FormControl>
<FormControl sx={{ flexGrow: 1 }}>
<FormLabel>Email</FormLabel>
<Input
size="sm"
type="email"
startDecorator={<EmailRoundedIcon />}
placeholder="email"
defaultValue="siriwatk@test.com"
sx={{ flexGrow: 1 }}
/>
</FormControl>
<div>
<CountrySelector />
</div>
<div>
<FormControl sx={{ display: { sm: 'contents' } }}>
<FormLabel>Timezone</FormLabel>
<Select
size="sm"
startDecorator={<AccessTimeFilledRoundedIcon />}
defaultValue="1"
>
<Option value="1">
Indochina Time (Bangkok){' '}
<Typography textColor="text.tertiary" sx={{ ml: 0.5 }}>
GMT+07:00
</Typography>
</Option>
<Option value="2">
Indochina Time (Ho Chi Minh City){' '}
<Typography textColor="text.tertiary" sx={{ ml: 0.5 }}>
GMT+07:00
</Typography>
</Option>
</Select>
</FormControl>
</div>
</Stack>
<CardOverflow sx={{ borderTop: '1px solid', borderColor: 'divider' }}>
<CardActions sx={{ alignSelf: 'flex-end', pt: 2 }}>
<Button size="sm" variant="outlined" color="neutral">
Cancel
</Button>
<Button size="sm" variant="solid">
Save
</Button>
</CardActions>
</CardOverflow>
</Card>
<Card>
<Box sx={{ mb: 1 }}>
<Typography level="title-md">Bio</Typography>
<Typography level="body-sm">
Write a short introduction to be displayed on your profile
</Typography>
</Box>
<Divider />
<Stack spacing={2} sx={{ my: 1 }}>
<EditorToolbar />
<Textarea
size="sm"
minRows={4}
sx={{ mt: 1.5 }}
defaultValue="I'm a software developer based in Bangkok, Thailand. My goal is to solve UI problems with neat CSS without using too much JavaScript."
/>
<FormHelperText sx={{ mt: 0.75, fontSize: 'xs' }}>
275 characters left
</FormHelperText>
</Stack>
<CardOverflow sx={{ borderTop: '1px solid', borderColor: 'divider' }}>
<CardActions sx={{ alignSelf: 'flex-end', pt: 2 }}>
<Button size="sm" variant="outlined" color="neutral">
Cancel
</Button>
<Button size="sm" variant="solid">
Save
</Button>
</CardActions>
</CardOverflow>
</Card>
<Card>
<Box sx={{ mb: 1 }}>
<Typography level="title-md">Portfolio projects</Typography>
<Typography level="body-sm">
Share a few snippets of your work.
</Typography>
</Box>
<Divider />
<Stack spacing={2} sx={{ my: 1 }}>
<DropZone />
<FileUpload
icon={<InsertDriveFileRoundedIcon />}
fileName="Tech design requirements.pdf"
fileSize="200 kB"
progress={100}
/>
<FileUpload
icon={<VideocamRoundedIcon />}
fileName="Dashboard prototype recording.mp4"
fileSize="16 MB"
progress={40}
/>
</Stack>
<CardOverflow sx={{ borderTop: '1px solid', borderColor: 'divider' }}>
<CardActions sx={{ alignSelf: 'flex-end', pt: 2 }}>
<Button size="sm" variant="outlined" color="neutral">
Cancel
</Button>
<Button size="sm" variant="solid">
Save
</Button>
</CardActions>
</CardOverflow>
</Card>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,324 @@
import * as React from 'react';
import GlobalStyles from '@mui/joy/GlobalStyles';
import Avatar from '@mui/joy/Avatar';
import Box from '@mui/joy/Box';
import Button from '@mui/joy/Button';
import Card from '@mui/joy/Card';
import Chip from '@mui/joy/Chip';
import Divider from '@mui/joy/Divider';
import IconButton from '@mui/joy/IconButton';
import Input from '@mui/joy/Input';
import LinearProgress from '@mui/joy/LinearProgress';
import List from '@mui/joy/List';
import ListItem from '@mui/joy/ListItem';
import ListItemButton, { listItemButtonClasses } from '@mui/joy/ListItemButton';
import ListItemContent from '@mui/joy/ListItemContent';
import Typography from '@mui/joy/Typography';
import Sheet from '@mui/joy/Sheet';
import Stack from '@mui/joy/Stack';
import SearchRoundedIcon from '@mui/icons-material/SearchRounded';
import HomeRoundedIcon from '@mui/icons-material/HomeRounded';
import DashboardRoundedIcon from '@mui/icons-material/DashboardRounded';
import ShoppingCartRoundedIcon from '@mui/icons-material/ShoppingCartRounded';
import AssignmentRoundedIcon from '@mui/icons-material/AssignmentRounded';
import QuestionAnswerRoundedIcon from '@mui/icons-material/QuestionAnswerRounded';
import GroupRoundedIcon from '@mui/icons-material/GroupRounded';
import SupportRoundedIcon from '@mui/icons-material/SupportRounded';
import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import LogoutRoundedIcon from '@mui/icons-material/LogoutRounded';
import BrightnessAutoRoundedIcon from '@mui/icons-material/BrightnessAutoRounded';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import ColorSchemeToggle from './ColorSchemeToggle';
import { closeSidebar } from '../utils';
function Toggler({
defaultExpanded = false,
renderToggle,
children,
}: {
defaultExpanded?: boolean;
children: React.ReactNode;
renderToggle: (params: {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}) => React.ReactNode;
}) {
const [open, setOpen] = React.useState(defaultExpanded);
return (
<React.Fragment>
{renderToggle({ open, setOpen })}
<Box
sx={[
{
display: 'grid',
transition: '0.2s ease',
'& > *': {
overflow: 'hidden',
},
},
open ? { gridTemplateRows: '1fr' } : { gridTemplateRows: '0fr' },
]}
>
{children}
</Box>
</React.Fragment>
);
}
export default function Sidebar() {
return (
<Sheet
className="Sidebar"
sx={{
position: { xs: 'fixed', md: 'sticky' },
transform: {
xs: 'translateX(calc(100% * (var(--SideNavigation-slideIn, 0) - 1)))',
md: 'none',
},
transition: 'transform 0.4s, width 0.4s',
zIndex: 10000,
height: '100dvh',
width: 'var(--Sidebar-width)',
top: 0,
p: 2,
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
gap: 2,
borderRight: '1px solid',
borderColor: 'divider',
}}
>
<GlobalStyles
styles={(theme) => ({
':root': {
'--Sidebar-width': '220px',
[theme.breakpoints.up('lg')]: {
'--Sidebar-width': '240px',
},
},
})}
/>
<Box
className="Sidebar-overlay"
sx={{
position: 'fixed',
zIndex: 9998,
top: 0,
left: 0,
width: '100vw',
height: '100vh',
opacity: 'var(--SideNavigation-slideIn)',
backgroundColor: 'var(--joy-palette-background-backdrop)',
transition: 'opacity 0.4s',
transform: {
xs: 'translateX(calc(100% * (var(--SideNavigation-slideIn, 0) - 1) + var(--SideNavigation-slideIn, 0) * var(--Sidebar-width, 0px)))',
lg: 'translateX(-100%)',
},
}}
onClick={() => closeSidebar()}
/>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<IconButton variant="soft" color="primary" size="sm">
<BrightnessAutoRoundedIcon />
</IconButton>
<Typography level="title-lg">Acme Co.</Typography>
<ColorSchemeToggle sx={{ ml: 'auto' }} />
</Box>
<Input size="sm" startDecorator={<SearchRoundedIcon />} placeholder="Search" />
<Box
sx={{
minHeight: 0,
overflow: 'hidden auto',
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
[`& .${listItemButtonClasses.root}`]: {
gap: 1.5,
},
}}
>
<List
size="sm"
sx={{
gap: 1,
'--List-nestedInsetStart': '30px',
'--ListItem-radius': (theme) => theme.vars.radius.sm,
}}
>
<ListItem>
<ListItemButton>
<HomeRoundedIcon />
<ListItemContent>
<Typography level="title-sm">Home</Typography>
</ListItemContent>
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton>
<DashboardRoundedIcon />
<ListItemContent>
<Typography level="title-sm">Dashboard</Typography>
</ListItemContent>
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton
role="menuitem"
component="a"
href="/joy-ui/getting-started/templates/order-dashboard/"
>
<ShoppingCartRoundedIcon />
<ListItemContent>
<Typography level="title-sm">Orders</Typography>
</ListItemContent>
</ListItemButton>
</ListItem>
<ListItem nested>
<Toggler
renderToggle={({ open, setOpen }) => (
<ListItemButton onClick={() => setOpen(!open)}>
<AssignmentRoundedIcon />
<ListItemContent>
<Typography level="title-sm">Tasks</Typography>
</ListItemContent>
<KeyboardArrowDownIcon
sx={[
open ? { transform: 'rotate(180deg)' } : { transform: 'none' },
]}
/>
</ListItemButton>
)}
>
<List sx={{ gap: 0.5 }}>
<ListItem sx={{ mt: 0.5 }}>
<ListItemButton>All tasks</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton>Backlog</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton>In progress</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton>Done</ListItemButton>
</ListItem>
</List>
</Toggler>
</ListItem>
<ListItem>
<ListItemButton
role="menuitem"
component="a"
href="/joy-ui/getting-started/templates/messages/"
>
<QuestionAnswerRoundedIcon />
<ListItemContent>
<Typography level="title-sm">Messages</Typography>
</ListItemContent>
<Chip size="sm" color="primary" variant="solid">
4
</Chip>
</ListItemButton>
</ListItem>
<ListItem nested>
<Toggler
defaultExpanded
renderToggle={({ open, setOpen }) => (
<ListItemButton onClick={() => setOpen(!open)}>
<GroupRoundedIcon />
<ListItemContent>
<Typography level="title-sm">Users</Typography>
</ListItemContent>
<KeyboardArrowDownIcon
sx={[
open ? { transform: 'rotate(180deg)' } : { transform: 'none' },
]}
/>
</ListItemButton>
)}
>
<List sx={{ gap: 0.5 }}>
<ListItem sx={{ mt: 0.5 }}>
<ListItemButton selected>My profile</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton>Create a new user</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton>Roles & permission</ListItemButton>
</ListItem>
</List>
</Toggler>
</ListItem>
</List>
<List
size="sm"
sx={{
mt: 'auto',
flexGrow: 0,
'--ListItem-radius': (theme) => theme.vars.radius.sm,
'--List-gap': '8px',
mb: 2,
}}
>
<ListItem>
<ListItemButton>
<SupportRoundedIcon />
Support
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton>
<SettingsRoundedIcon />
Settings
</ListItemButton>
</ListItem>
</List>
<Card
invertedColors
variant="soft"
color="warning"
size="sm"
sx={{ boxShadow: 'none' }}
>
<Stack
direction="row"
sx={{ justifyContent: 'space-between', alignItems: 'center' }}
>
<Typography level="title-sm">Used space</Typography>
<IconButton size="sm">
<CloseRoundedIcon />
</IconButton>
</Stack>
<Typography level="body-xs">
Your team has used 80% of your available space. Need more?
</Typography>
<LinearProgress variant="outlined" value={80} determinate sx={{ my: 1 }} />
<Button size="sm" variant="solid">
Upgrade plan
</Button>
</Card>
</Box>
<Divider />
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Avatar
variant="outlined"
size="sm"
src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=286"
/>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography level="title-sm">Siriwat K.</Typography>
<Typography level="body-xs">siriwatk@test.com</Typography>
</Box>
<IconButton size="sm" variant="plain" color="neutral">
<LogoutRoundedIcon />
</IconButton>
</Box>
</Sheet>
);
}

View File

@@ -0,0 +1,99 @@
import * as React from 'react';
export type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error';
// Cached script statuses
const cachedScriptStatuses: Record<string, UseScriptStatus | undefined> = {};
/**
* Simplified version of https://usehooks-ts.com/react-hook/use-script
*/
function getScriptNode(src: string) {
const node: HTMLScriptElement | null = document.querySelector(`script[src="${src}"]`);
const status = node?.getAttribute('data-status') as UseScriptStatus | undefined;
return {
node,
status,
};
}
export default function useScript(src: string): UseScriptStatus {
const [status, setStatus] = React.useState<UseScriptStatus>(() => {
if (typeof window === 'undefined') {
// SSR Handling - always return 'loading'
return 'loading';
}
return cachedScriptStatuses[src] ?? 'loading';
});
React.useEffect(() => {
const cachedScriptStatus = cachedScriptStatuses[src];
if (cachedScriptStatus === 'ready' || cachedScriptStatus === 'error') {
// If the script is already cached, set its status immediately
setStatus(cachedScriptStatus);
return;
}
// Fetch existing script element by src
// It may have been added by another instance of this hook
const script = getScriptNode(src);
let scriptNode = script.node;
if (!scriptNode) {
// Create script element and add it to document body
scriptNode = document.createElement('script');
scriptNode.src = src;
scriptNode.async = true;
scriptNode.setAttribute('data-status', 'loading');
document.body.appendChild(scriptNode);
// Store status in attribute on script
// This can be read by other instances of this hook
const setAttributeFromEvent = (event: Event) => {
const scriptStatus: UseScriptStatus = event.type === 'load' ? 'ready' : 'error';
scriptNode?.setAttribute('data-status', scriptStatus);
};
scriptNode.addEventListener('load', setAttributeFromEvent);
scriptNode.addEventListener('error', setAttributeFromEvent);
} else {
// Grab existing script status from attribute and set to state.
setStatus(script.status ?? cachedScriptStatus ?? 'loading');
}
// Script event handler to update status in state
// Note: Even if the script already exists we still need to add
// event handlers to update the state for *this* hook instance.
const setStateFromEvent = (event: Event) => {
const newStatus = event.type === 'load' ? 'ready' : 'error';
setStatus(newStatus);
cachedScriptStatuses[src] = newStatus;
};
// Add event listeners
scriptNode.addEventListener('load', setStateFromEvent);
scriptNode.addEventListener('error', setStateFromEvent);
// Remove event listeners on cleanup
// eslint-disable-next-line consistent-return
return () => {
if (scriptNode) {
scriptNode.removeEventListener('load', setStateFromEvent);
scriptNode.removeEventListener('error', setStateFromEvent);
}
if (scriptNode) {
try {
scriptNode.remove();
} catch (error) {
// ignore error
}
}
};
}, [src]);
return status;
}

View File

@@ -0,0 +1,26 @@
export function openSidebar() {
if (typeof window !== 'undefined') {
document.body.style.overflow = 'hidden';
document.documentElement.style.setProperty('--SideNavigation-slideIn', '1');
}
}
export function closeSidebar() {
if (typeof window !== 'undefined') {
document.documentElement.style.removeProperty('--SideNavigation-slideIn');
document.body.style.removeProperty('overflow');
}
}
export function toggleSidebar() {
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
const slideIn = window
.getComputedStyle(document.documentElement)
.getPropertyValue('--SideNavigation-slideIn');
if (slideIn) {
closeSidebar();
} else {
openSidebar();
}
}
}