🚀 release v3.0.0

This commit is contained in:
Amruth Pillai
2022-03-06 22:48:29 +01:00
parent 00505a9e5d
commit 9c1380f401
373 changed files with 12050 additions and 15783 deletions
@@ -0,0 +1,15 @@
.header {
@apply mb-4 flex items-center justify-between;
.label {
@apply text-base font-semibold;
}
}
.inputGrid {
@apply grid grid-cols-2 gap-4;
.delete {
@apply opacity-25 hover:opacity-75;
}
}
+70
View File
@@ -0,0 +1,70 @@
import { Add, Delete } from '@mui/icons-material';
import { IconButton, InputAdornment, TextField } from '@mui/material';
import get from 'lodash/get';
import { useEffect, useState } from 'react';
import { FieldError } from 'react-hook-form';
import styles from './ArrayInput.module.scss';
type Props = {
label: string;
value: string[];
className?: string;
onChange: (event: any) => void;
errors?: FieldError | FieldError[];
};
const ArrayInput: React.FC<Props> = ({ value, label, onChange, errors, className }) => {
const [items, setItems] = useState<string[]>(value);
const onAdd = () => setItems([...items, '']);
const onDelete = (index: number) => setItems(items.filter((_, idx) => idx !== index));
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, index: number) => {
const tempItems = [...items];
tempItems[index] = event.target.value;
setItems(tempItems);
};
useEffect(() => {
onChange(items);
}, [onChange, items]);
return (
<div className={className}>
<header className={styles.header}>
<h3 className={styles.label}>
{label} <small>({items.length})</small>
</h3>
<IconButton onClick={onAdd}>
<Add />
</IconButton>
</header>
<div className={styles.inputGrid}>
{items.map((value, index) => (
<TextField
key={index}
value={value}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => handleChange(event, index)}
error={!!get(errors, index, false)}
helperText={get(errors, `${index}.message`, '')}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton edge="end" onClick={() => onDelete(index)} className={styles.delete}>
<Delete />
</IconButton>
</InputAdornment>
),
}}
/>
))}
</div>
</div>
);
};
export default ArrayInput;
@@ -0,0 +1,3 @@
.avatar {
@apply cursor-pointer rounded-full;
}
+70
View File
@@ -0,0 +1,70 @@
import { Divider, IconButton, Menu, MenuItem } from '@mui/material';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { logout } from '@/store/auth/authSlice';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import getGravatarUrl from '@/utils/getGravatarUrl';
import styles from './Avatar.module.scss';
type Props = {
size?: number;
};
const Avatar: React.FC<Props> = ({ size = 64 }) => {
const router = useRouter();
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
const user = useAppSelector((state) => state.auth.user);
const email = user?.email || '';
const handleOpen = (event: React.MouseEvent<Element>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleLogout = () => {
dispatch(logout());
handleClose();
router.push('/');
};
return (
<>
<IconButton onClick={handleOpen}>
<Image
width={size}
height={size}
alt={user?.name}
className={styles.avatar}
src={getGravatarUrl(email, size)}
/>
</IconButton>
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
<MenuItem>
<div>
<span className="text-xs opacity-50">{t('common.avatar.menu.greeting')}</span>
<p>{user?.name}</p>
</div>
</MenuItem>
<Divider />
<MenuItem onClick={handleLogout}>{t('common.avatar.menu.logout')}</MenuItem>
</Menu>
</>
);
};
export default Avatar;
@@ -0,0 +1,32 @@
.content {
@apply rounded p-6 text-sm shadow lg:w-1/2 xl:w-2/5;
@apply absolute inset-4 sm:inset-x-4 sm:inset-y-auto lg:inset-auto;
@apply overflow-scroll bg-neutral-50 dark:bg-neutral-900 lg:overflow-auto;
}
.header {
@apply flex items-center justify-between;
@apply w-full border-b pb-5 dark:border-white/10;
> div {
@apply flex items-center gap-2;
}
button {
@apply flex items-center justify-center;
@apply rotate-0 transition-transform hover:rotate-90;
}
h1 {
@apply text-base font-medium;
}
}
.body {
@apply grid gap-4 pt-4 pb-6;
}
.footer {
@apply flex items-center justify-end gap-x-4;
@apply w-full border-t pt-5 dark:border-white/10;
}
+56
View File
@@ -0,0 +1,56 @@
import { Close as CloseIcon } from '@mui/icons-material';
import { Fade, IconButton, Modal } from '@mui/material';
import { useRouter } from 'next/router';
import styles from './BaseModal.module.scss';
type Props = {
icon?: React.ReactNode;
isOpen: boolean;
heading: string;
handleClose: () => void;
footerChildren?: React.ReactNode;
};
const BaseModal: React.FC<Props> = ({ icon, isOpen, heading, children, handleClose, footerChildren }) => {
const router = useRouter();
const { pathname, query } = router;
const onClose = () => {
router.push({ pathname, query }, '');
handleClose();
};
return (
<Modal
open={isOpen}
onClose={onClose}
closeAfterTransition
aria-labelledby={heading}
classes={{ root: 'flex items-center justify-center' }}
>
<Fade in={isOpen}>
<div className={styles.content}>
<header className={styles.header}>
<div>
{icon}
{icon && <span className="mx-1 opacity-25">/</span>}
<h1>{heading}</h1>
</div>
<IconButton size="small" onClick={onClose}>
<CloseIcon sx={{ fontSize: 18 }} />
</IconButton>
</header>
<div className={styles.body}>{children}</div>
{footerChildren ? <footer className={styles.footer}>{footerChildren}</footer> : null}
</div>
</Fade>
</Modal>
);
};
export default BaseModal;
+20
View File
@@ -0,0 +1,20 @@
import { Avatar, IconButton } from '@mui/material';
import isFunction from 'lodash/isFunction';
type Props = {
color: string;
size?: number;
onClick?: (color: string) => void;
};
const ColorAvatar: React.FC<Props> = ({ color, size = 20, onClick }) => {
const handleClick = () => isFunction(onClick) && onClick(color);
return (
<IconButton onClick={handleClick}>
<Avatar sx={{ bgcolor: color, width: size, height: size }}> </Avatar>
</IconButton>
);
};
export default ColorAvatar;
+68
View File
@@ -0,0 +1,68 @@
import { Popover, TextField } from '@mui/material';
import React, { useMemo, useState } from 'react';
import { HexColorPicker } from 'react-colorful';
import { hexColorPattern } from '@/config/colors';
import ColorAvatar from './ColorAvatar';
type Props = {
label: string;
color: string;
className?: string;
onChange: (color: string) => void;
};
const ColorPicker: React.FC<Props> = ({ label, color, onChange, className }) => {
const isValid = useMemo(() => hexColorPattern.test(color), [color]);
const [anchorEl, setAnchorEl] = useState<HTMLInputElement | null>(null);
const isOpen = Boolean(anchorEl);
const handleOpen = (event: React.MouseEvent<HTMLInputElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (hexColorPattern.test(event.target.value)) {
onChange(event.target.value);
}
};
return (
<>
<TextField
label={label}
value={color}
error={!isValid}
onClick={handleOpen}
onChange={handleChange}
className={className}
InputProps={{
startAdornment: (
<div className="mr-2">
<ColorAvatar color={color} />
</div>
),
}}
/>
<Popover
open={isOpen}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<HexColorPicker color={color} onChange={onChange} className="overflow-hidden" />
</Popover>
</>
);
};
export default ColorPicker;
+26
View File
@@ -0,0 +1,26 @@
import clsx from 'clsx';
import { Trans, useTranslation } from 'next-i18next';
type Props = {
className?: string;
};
const Footer: React.FC<Props> = ({ className }) => {
const { t } = useTranslation();
return (
<footer className={clsx('text-xs', className)}>
<p>{t('common.footer.license')}</p>
<p>
<Trans t={t} i18nKey="common.footer.credit">
A passion project by
<a href="https://www.amruthpillai.com/" target="_blank" rel="noreferrer">
Amruth Pillai
</a>
</Trans>
</p>
</footer>
);
};
export default Footer;
@@ -0,0 +1,17 @@
.container {
@apply flex items-center justify-between;
h1 {
@apply text-2xl;
}
.actions {
@apply flex gap-2 opacity-75 transition-opacity lg:opacity-50 dark:lg:opacity-25;
}
&:hover {
.actions {
@apply opacity-75;
}
}
}
+100
View File
@@ -0,0 +1,100 @@
import { Check, Delete, DriveFileRenameOutline, Grade, Visibility, VisibilityOff } from '@mui/icons-material';
import { IconButton, TextField, Tooltip } from '@mui/material';
import clsx from 'clsx';
import get from 'lodash/get';
import { useTranslation } from 'next-i18next';
import React, { useMemo, useState } from 'react';
import sections from '@/config/sections';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { deleteSection, setResumeState } from '@/store/resume/resumeSlice';
import styles from './Heading.module.scss';
type Props = {
path: string;
name?: string;
isEditable?: boolean;
isHideable?: boolean;
isDeletable?: boolean;
action?: React.ReactNode;
};
const Heading: React.FC<Props> = ({
path,
name,
isEditable = false,
isHideable = false,
isDeletable = false,
action,
}) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const heading = useAppSelector((state) => get(state.resume, `${path}.name`, name));
const visibility = useAppSelector((state) => get(state.resume, `${path}.visible`, true));
const [editMode, setEditMode] = useState(false);
const id = useMemo(() => path && path.split('.').at(-1), [path]);
const icon = sections.find((x) => x.id === id)?.icon || <Grade />;
const toggleVisibility = () => {
dispatch(setResumeState({ path: `${path}.visible`, value: !visibility }));
};
const toggleEditMode = () => setEditMode(!editMode);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setResumeState({ path: `${path}.name`, value: event.target.value }));
};
const handleDelete = () => {
dispatch(deleteSection({ path }));
};
return (
<div className={styles.container}>
<div className="flex w-full items-center gap-3">
<div className="opacity-50">{icon}</div>
{editMode ? (
<TextField size="small" value={heading} className="w-3/4" onChange={handleChange} />
) : (
<h1>{heading}</h1>
)}
</div>
<div
className={clsx(styles.actions, {
'!opacity-75': editMode,
})}
>
{isEditable && (
<Tooltip title={t<string>('builder.common.tooltip.rename-section')}>
<IconButton onClick={toggleEditMode}>{editMode ? <Check /> : <DriveFileRenameOutline />}</IconButton>
</Tooltip>
)}
{isHideable && (
<Tooltip title={t<string>('builder.common.tooltip.toggle-visibility')}>
<IconButton onClick={toggleVisibility}>{visibility ? <Visibility /> : <VisibilityOff />}</IconButton>
</Tooltip>
)}
{isDeletable && (
<Tooltip title={t<string>('builder.common.tooltip.delete-section')}>
<IconButton onClick={handleDelete}>
<Delete />
</IconButton>
</Tooltip>
)}
{action}
</div>
</div>
);
};
export default Heading;
@@ -0,0 +1,7 @@
.container {
@apply rounded-lg border dark:border-neutral-50/10;
.empty {
@apply py-8 text-center;
}
}
+95
View File
@@ -0,0 +1,95 @@
import { ListItem as ListItemType } from '@reactive-resume/schema';
import clsx from 'clsx';
import get from 'lodash/get';
import isArray from 'lodash/isArray';
import isEmpty from 'lodash/isEmpty';
import isFunction from 'lodash/isFunction';
import { useTranslation } from 'next-i18next';
import { useCallback } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { deleteItem, setResumeState } from '@/store/resume/resumeSlice';
import styles from './List.module.scss';
import ListItem from './ListItem';
type Props = {
path: string;
titleKey?: string;
subtitleKey?: string;
onEdit?: (item: ListItemType) => void;
onDuplicate?: (item: ListItemType) => void;
className?: string;
};
const List: React.FC<Props> = ({
path,
titleKey = 'title',
subtitleKey = 'subtitle',
onEdit,
onDuplicate,
className,
}) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const list: Array<ListItemType> = useAppSelector((state) => get(state.resume, path, []));
const handleEdit = (item: ListItemType) => {
isFunction(onEdit) && onEdit(item);
};
const handleDuplicate = (item: ListItemType) => {
isFunction(onDuplicate) && onDuplicate(item);
};
const handleDelete = (item: ListItemType) => {
dispatch(deleteItem({ path, value: item }));
};
const handleMove = useCallback(
(dragIndex: number, hoverIndex: number) => {
const dragItem = list[dragIndex];
const newList = [...list];
newList.splice(dragIndex, 1);
newList.splice(hoverIndex, 0, dragItem);
dispatch(setResumeState({ path, value: newList }));
},
[list, dispatch, path]
);
return (
<DndProvider backend={HTML5Backend}>
<div className={clsx(styles.container, className)}>
{isEmpty(list) && <div className={styles.empty}>{t('builder.common.list.empty-text')}</div>}
{list.map((item, index) => {
const title = get(item, titleKey, '');
const subtitleObj = get(item, subtitleKey);
const subtitle: string = isArray(subtitleObj) ? subtitleObj.join(', ') : subtitleObj;
return (
<ListItem
key={item.id}
item={item}
index={index}
title={title}
subtitle={subtitle}
onMove={handleMove}
onEdit={handleEdit}
onDelete={handleDelete}
onDuplicate={handleDuplicate}
/>
);
})}
</div>
</DndProvider>
);
};
export default List;
@@ -0,0 +1,18 @@
.item {
@apply flex items-center justify-between;
@apply py-5 pl-5 pr-2;
@apply border-b border-neutral-900/10 last:border-0 dark:border-neutral-50/10;
@apply cursor-move transition-opacity;
.meta {
@apply grid gap-1;
.title {
@apply font-semibold;
}
.subtitle {
@apply text-xs opacity-50;
}
}
}
+157
View File
@@ -0,0 +1,157 @@
import { DeleteOutline, DriveFileRenameOutline, FileCopy, MoreVert } from '@mui/icons-material';
import { Divider, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip } from '@mui/material';
import { ListItem as ListItemType } from '@reactive-resume/schema';
import clsx from 'clsx';
import isFunction from 'lodash/isFunction';
import React, { useRef, useState } from 'react';
import { DropTargetMonitor, useDrag, useDrop, XYCoord } from 'react-dnd';
import styles from './ListItem.module.scss';
interface DragItem {
id: string;
type: string;
index: number;
}
type Props = {
item: ListItemType;
index: number;
title: string;
subtitle?: string;
onMove?: (dragIndex: number, hoverIndex: number) => void;
onEdit?: (item: ListItemType) => void;
onDelete?: (item: ListItemType) => void;
onDuplicate?: (item: ListItemType) => void;
};
const ListItem: React.FC<Props> = ({ item, index, title, subtitle, onMove, onEdit, onDelete, onDuplicate }) => {
const ref = useRef<HTMLDivElement>(null);
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
const [{ handlerId }, drop] = useDrop<DragItem, any, any>({
accept: 'ListItem',
collect(monitor) {
return { handlerId: monitor.getHandlerId() };
},
hover(item: DragItem, monitor: DropTargetMonitor) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
if (dragIndex === hoverIndex) {
return;
}
const hoverBoundingRect = ref.current?.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
isFunction(onMove) && onMove(dragIndex, hoverIndex);
item.index = hoverIndex;
},
});
const [{ isDragging }, drag] = useDrag({
type: 'ListItem',
item: () => {
return { id: item.id, index };
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const handleOpen = (event: React.MouseEvent<Element>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleEdit = (item: ListItemType) => {
isFunction(onEdit) && onEdit(item);
handleClose();
};
const handleDelete = (item: ListItemType) => {
isFunction(onDelete) && onDelete(item);
handleClose();
};
const handleDuplicate = (item: ListItemType) => {
isFunction(onDuplicate) && onDuplicate(item);
handleClose();
};
drag(drop(ref));
return (
<div
ref={ref}
data-handler-id={handlerId}
className={clsx(styles.item, {
['opacity-25']: isDragging,
})}
>
<div className={styles.meta}>
<h1 className={styles.title}>{title}</h1>
<h2 className={styles.subtitle}>{subtitle}</h2>
</div>
<div>
<IconButton onClick={handleOpen}>
<MoreVert />
</IconButton>
<Menu anchorEl={anchorEl} onClose={handleClose} open={Boolean(anchorEl)}>
<MenuItem onClick={() => handleEdit(item)}>
<ListItemIcon>
<DriveFileRenameOutline className="scale-90" />
</ListItemIcon>
<ListItemText>Edit</ListItemText>
</MenuItem>
<MenuItem onClick={() => handleDuplicate(item)}>
<ListItemIcon>
<FileCopy className="scale-90" />
</ListItemIcon>
<ListItemText>Duplicate</ListItemText>
</MenuItem>
<Divider />
<Tooltip
arrow
placement="right"
title="Are you sure you want to delete this item? This is an irreversible action."
>
<div>
<MenuItem onClick={() => handleDelete(item)}>
<ListItemIcon>
<DeleteOutline className="scale-90" />
</ListItemIcon>
<ListItemText>Delete</ListItemText>
</MenuItem>
</div>
</Tooltip>
</Menu>
</div>
</div>
);
};
export default ListItem;
@@ -0,0 +1,32 @@
.loading {
animation: progress 2s linear infinite;
@apply fixed top-0 z-50;
@apply bg-primary-500 shadow-primary-500/50 h-0.5 w-screen shadow;
}
@keyframes progress {
0% {
left: 0%;
right: 100%;
width: 0;
}
10% {
left: 0%;
right: 75%;
width: 25%;
}
90% {
left: 75%;
right: 0%;
width: 25%;
}
100% {
left: 100%;
right: 0%;
width: 0;
}
}
+18
View File
@@ -0,0 +1,18 @@
import { useRouter } from 'next/router';
import { useIsFetching, useIsMutating } from 'react-query';
import styles from './Loading.module.scss';
const Loading: React.FC = () => {
const { isReady } = useRouter();
const isFetching = useIsFetching();
const isMutating = useIsMutating();
if (!isFetching && !isMutating && isReady) {
return null;
}
return <div className={styles.loading} />;
};
export default Loading;
+11
View File
@@ -0,0 +1,11 @@
import Image from 'next/image';
type Props = {
size?: 256 | 64 | 48 | 40 | 32;
};
const Logo: React.FC<Props> = ({ size = 64 }) => {
return <Image alt="Reactive Resume" src="/images/logo.svg" className="rounded" width={size} height={size} />;
};
export default Logo;
+21
View File
@@ -0,0 +1,21 @@
import clsx from 'clsx';
import { isEmpty } from 'lodash';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
type Props = {
children?: string;
className?: string;
};
const Markdown: React.FC<Props> = ({ className, children }) => {
if (!children || isEmpty(children)) return null;
return (
<ReactMarkdown remarkPlugins={[remarkGfm]} className={clsx('markdown', className)}>
{children}
</ReactMarkdown>
);
};
export default Markdown;
@@ -0,0 +1,20 @@
import { Link } from '@mui/material';
import { Trans, useTranslation } from 'next-i18next';
const MarkdownSupported: React.FC = () => {
const { t } = useTranslation();
return (
<span className="inline-block pt-1 opacity-75">
<Trans t={t} i18nKey="common.markdown.help-text">
This section supports
<Link href="https://www.markdownguide.org/cheat-sheet/" target="_blank" rel="noreferrer">
markdown
</Link>
formatting.
</Trans>
</span>
);
};
export default MarkdownSupported;
+5
View File
@@ -0,0 +1,5 @@
import dynamic from 'next/dynamic';
const NoSSR: React.FC = ({ children }) => <>{children}</>;
export default dynamic(() => Promise.resolve(NoSSR), { ssr: false });
+51
View File
@@ -0,0 +1,51 @@
import { TextField } from '@mui/material';
import get from 'lodash/get';
import { useEffect, useState } from 'react';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setResumeState } from '@/store/resume/resumeSlice';
import MarkdownSupported from './MarkdownSupported';
interface Props {
type?: 'text' | 'textarea';
label: string;
path: string;
className?: string;
markdownSupported?: boolean;
}
const ResumeInput: React.FC<Props> = ({ type = 'text', label, path, className, markdownSupported = false }) => {
const dispatch = useAppDispatch();
const stateValue = useAppSelector((state) => get(state.resume, path, ''));
useEffect(() => {
setValue(stateValue);
}, [stateValue]);
const [value, setValue] = useState<string>(stateValue);
const onChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setValue(event.target.value);
dispatch(setResumeState({ path, value: event.target.value }));
};
if (type === 'textarea') {
return (
<TextField
rows={5}
multiline
label={label}
value={value}
onChange={onChange}
className={className}
helperText={markdownSupported && <MarkdownSupported />}
/>
);
}
return <TextField type={type} label={label} value={value} onChange={onChange} className={className} />;
};
export default ResumeInput;
+50
View File
@@ -0,0 +1,50 @@
import { styled, Switch } from '@mui/material';
const ThemeSwitch = styled(Switch)(({ theme }) => ({
width: 62,
height: 34,
padding: 7,
'& .MuiSwitch-switchBase': {
margin: 1,
padding: 0,
transform: 'translateX(6px)',
'&.Mui-checked': {
color: '#fff',
transform: 'translateX(22px)',
'& .MuiSwitch-thumb:before': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
'#fff'
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`,
},
'& + .MuiSwitch-track': {
opacity: 1,
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
},
},
},
'& .MuiSwitch-thumb': {
backgroundColor: theme.palette.mode === 'dark' ? '#003892' : '#001e3c',
width: 32,
height: 32,
'&:before': {
content: "''",
position: 'absolute',
width: '100%',
height: '100%',
left: 0,
top: 0,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
'#fff'
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`,
},
},
'& .MuiSwitch-track': {
opacity: 1,
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
borderRadius: 20 / 2,
},
}));
export default ThemeSwitch;