107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
|
import { Autocomplete, Skeleton, Slider, TextField } from '@mui/material';
|
||
|
import { Font, TypeCategory, TypeProperty, Typography as TypographyType } from '@reactive-resume/schema';
|
||
|
import get from 'lodash/get';
|
||
|
import isEmpty from 'lodash/isEmpty';
|
||
|
import { useTranslation } from 'next-i18next';
|
||
|
import { useQuery } from 'react-query';
|
||
|
|
||
|
import Heading from '@/components/shared/Heading';
|
||
|
import { FONTS_QUERY } from '@/constants/index';
|
||
|
import { fetchFonts } from '@/services/fonts';
|
||
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||
|
import { setResumeState } from '@/store/resume/resumeSlice';
|
||
|
|
||
|
import styles from './Typography.module.scss';
|
||
|
|
||
|
const TypographySkeleton: React.FC = () => (
|
||
|
<>
|
||
|
<Skeleton variant="text" />
|
||
|
<div className={styles.container}>
|
||
|
<Skeleton variant="rectangular" height={60} />
|
||
|
<Skeleton variant="rectangular" height={60} />
|
||
|
</div>
|
||
|
</>
|
||
|
);
|
||
|
|
||
|
type WidgetProps = {
|
||
|
label: string;
|
||
|
category: TypeCategory;
|
||
|
};
|
||
|
|
||
|
const Widgets: React.FC<WidgetProps> = ({ label, category }) => {
|
||
|
const { t } = useTranslation();
|
||
|
|
||
|
const dispatch = useAppDispatch();
|
||
|
|
||
|
const { family, size } = useAppSelector<TypographyType>((state) => get(state.resume, 'metadata.typography'));
|
||
|
|
||
|
const { data: fonts } = useQuery(FONTS_QUERY, fetchFonts, {
|
||
|
select: (fonts) => fonts.sort((a, b) => a.category.localeCompare(b.category)),
|
||
|
});
|
||
|
|
||
|
const handleChange = (property: TypeProperty, value: number | Font) => {
|
||
|
if (value === null) return;
|
||
|
|
||
|
dispatch(
|
||
|
setResumeState({
|
||
|
path: `metadata.typography.${property}.${category}`,
|
||
|
value: property === 'family' ? (value as Font).family : value,
|
||
|
})
|
||
|
);
|
||
|
};
|
||
|
|
||
|
if (isEmpty(fonts)) return <TypographySkeleton />;
|
||
|
|
||
|
return (
|
||
|
<>
|
||
|
<h5 className={styles.subheading}>{label}</h5>
|
||
|
|
||
|
<div className={styles.container}>
|
||
|
<div className={styles.slider}>
|
||
|
<Slider
|
||
|
min={12}
|
||
|
max={36}
|
||
|
step={1}
|
||
|
marks={[
|
||
|
{ value: 12, label: '12px' },
|
||
|
{ value: 24, label: t('builder.rightSidebar.sections.typography.form.font-size.label') },
|
||
|
{ value: 36, label: '36px' },
|
||
|
]}
|
||
|
valueLabelDisplay="auto"
|
||
|
value={size[category]}
|
||
|
onChange={(_, size: number) => handleChange('size', size)}
|
||
|
/>
|
||
|
</div>
|
||
|
|
||
|
<Autocomplete<Font, false, boolean, false>
|
||
|
options={fonts}
|
||
|
disableClearable={true}
|
||
|
groupBy={(font) => font.category}
|
||
|
getOptionLabel={(font) => font.family}
|
||
|
isOptionEqualToValue={(a, b) => a.family === b.family}
|
||
|
value={fonts.find((font) => font.family === family[category])}
|
||
|
onChange={(_, font: Font) => handleChange('family', font)}
|
||
|
renderInput={(params) => (
|
||
|
<TextField {...params} label={t('builder.rightSidebar.sections.typography.form.font-family.label')} />
|
||
|
)}
|
||
|
/>
|
||
|
</div>
|
||
|
</>
|
||
|
);
|
||
|
};
|
||
|
|
||
|
const Typography = () => {
|
||
|
const { t } = useTranslation();
|
||
|
|
||
|
return (
|
||
|
<>
|
||
|
<Heading path="metadata.typography" name={t('builder.rightSidebar.sections.typography.heading')} />
|
||
|
|
||
|
<Widgets label={t('builder.rightSidebar.sections.typography.widgets.headings.label')} category="heading" />
|
||
|
<Widgets label={t('builder.rightSidebar.sections.typography.widgets.body.label')} category="body" />
|
||
|
</>
|
||
|
);
|
||
|
};
|
||
|
|
||
|
export default Typography;
|