diff --git a/client/public/images/templates/leafish.jpg b/client/public/images/templates/leafish.jpg new file mode 100644 index 00000000..5dc8bf77 Binary files /dev/null and b/client/public/images/templates/leafish.jpg differ diff --git a/client/templates/Leafish/Leafish.module.scss b/client/templates/Leafish/Leafish.module.scss new file mode 100644 index 00000000..f78fdd46 --- /dev/null +++ b/client/templates/Leafish/Leafish.module.scss @@ -0,0 +1,9 @@ +.page {} + +.container { + @apply grid grid-cols-2 gap-8 px-6 py-4; + + .column { + @apply col-span-1 flex flex-col; + } +} diff --git a/client/templates/Leafish/Leafish.tsx b/client/templates/Leafish/Leafish.tsx new file mode 100644 index 00000000..e1c5602e --- /dev/null +++ b/client/templates/Leafish/Leafish.tsx @@ -0,0 +1,28 @@ +import { useMemo } from 'react'; + +import { useAppSelector } from '@/store/hooks'; +import { PageProps } from '@/utils/template'; + +import { getSectionById } from '../sectionMap'; +import styles from './Leafish.module.scss'; +import Masthead from './widgets/Masthead'; +import Section from './widgets/Section'; + +const Leafish: React.FC = ({ page }) => { + const isFirstPage = useMemo(() => page === 0, [page]); + + const layout: string[][] = useAppSelector((state) => state.resume.metadata.layout[page]); + + return ( +
+ {isFirstPage && } + +
+
{layout[0].map((key) => getSectionById(key, Section))}
+
{layout[1].map((key) => getSectionById(key, Section))}
+
+
+ ); +}; + +export default Leafish; diff --git a/client/templates/Leafish/widgets/Heading.tsx b/client/templates/Leafish/widgets/Heading.tsx new file mode 100644 index 00000000..5daaa5a0 --- /dev/null +++ b/client/templates/Leafish/widgets/Heading.tsx @@ -0,0 +1,19 @@ +import { Theme } from '@reactive-resume/schema'; +import get from 'lodash/get'; + +import { useAppSelector } from '@/store/hooks'; + +const Heading: React.FC = ({ children }) => { + const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); + + return ( +

+ {children} +

+ ); +}; + +export default Heading; diff --git a/client/templates/Leafish/widgets/Masthead.tsx b/client/templates/Leafish/widgets/Masthead.tsx new file mode 100644 index 00000000..58bdafe6 --- /dev/null +++ b/client/templates/Leafish/widgets/Masthead.tsx @@ -0,0 +1,75 @@ +import { Email, Phone, Public, Room } from '@mui/icons-material'; +import { alpha } from '@mui/material'; +import { Theme } from '@reactive-resume/schema'; +import get from 'lodash/get'; +import isEmpty from 'lodash/isEmpty'; + +import { useAppSelector } from '@/store/hooks'; +import DataDisplay from '@/templates/shared/DataDisplay'; +import getProfileIcon from '@/utils/getProfileIcon'; +import { addHttp, formatLocation, getPhotoClassNames } from '@/utils/template'; + +const Masthead: React.FC = () => { + const { name, photo, headline, summary, email, phone, website, location, profiles } = useAppSelector( + (state) => state.resume.basics + ); + const theme: Theme = useAppSelector((state) => get(state.resume, 'metadata.theme', {})); + + return ( +
+
+
+

{name}

+

+ {headline} +

+

+ {summary} +

+
+ + {photo.visible && !isEmpty(photo.url) && ( + {name} + )} +
+
+ } link={`mailto:${email}`}> + {email} + + + } link={`tel:${phone}`}> + {phone} + + + } link={addHttp(website)}> + {website} + + + }>{formatLocation(location)} + + {profiles.map(({ id, username, network, url }) => ( + + {username} + + ))} +
+
+ ); +}; + +export default Masthead; diff --git a/client/templates/Leafish/widgets/Section.tsx b/client/templates/Leafish/widgets/Section.tsx new file mode 100644 index 00000000..db0ad159 --- /dev/null +++ b/client/templates/Leafish/widgets/Section.tsx @@ -0,0 +1,127 @@ +import { Email, Link, Phone } from '@mui/icons-material'; +import { ListItem, Section as SectionType } from '@reactive-resume/schema'; +import get from 'lodash/get'; +import isArray from 'lodash/isArray'; +import isEmpty from 'lodash/isEmpty'; + +import Markdown from '@/components/shared/Markdown'; +import { useAppSelector } from '@/store/hooks'; +import { SectionProps } from '@/templates/sectionMap'; +import DataDisplay from '@/templates/shared/DataDisplay'; +import { formatDateString } from '@/utils/date'; +import { parseListItemPath } from '@/utils/template'; + +import Heading from './Heading'; + +const Section: React.FC = ({ + path, + titlePath = 'title', + subtitlePath = 'subtitle', + headlinePath = 'headline', + keywordsPath = 'keywords', +}) => { + const section: SectionType = useAppSelector((state) => get(state.resume, path, {})); + const dateFormat: string = useAppSelector((state) => get(state.resume, 'metadata.date.format')); + const primaryColor: string = useAppSelector((state) => get(state.resume, 'metadata.theme.primary')); + + if (!section.visible) return null; + + if (isArray(section.items) && isEmpty(section.items)) return null; + + return ( +
+ {section.name} + +
+ {section.items.map((item: ListItem) => { + const id = item.id, + title = parseListItemPath(item, titlePath), + subtitle = parseListItemPath(item, subtitlePath), + headline = parseListItemPath(item, headlinePath), + keywords: string[] = get(item, keywordsPath), + url: string = get(item, 'url'), + summary: string = get(item, 'summary'), + level: string = get(item, 'level'), + levelNum: number = get(item, 'levelNum'), + phone: string = get(item, 'phone'), + email: string = get(item, 'email'), + date = formatDateString(get(item, 'date'), dateFormat); + + return ( +
+
+ {title &&
{title}
} + {subtitle &&
{subtitle}
} + {date && ( +
+ ({date}) +
+ )} + {headline &&
{headline}
} +
+ + {(level || levelNum > 0) && ( +
+ {level && {level}} + {levelNum > 0 && ( +
+ {Array.from(Array(5).keys()).map((_, index) => ( +
index ? primaryColor : '', + }} + /> + ))} +
+ )} +
+ )} + + {summary && ( +
+
+ Overview +
+ {summary} +
+ )} + + {url && ( + } link={url} className="text-xs"> + {url} + + )} + + {keywords &&
{keywords.join(', ')}
} + + {(phone || email) && ( +
+ {phone && ( + } link={`tel:${phone}`}> + {phone} + + )} + + {email && ( + } link={`mailto:${email}`}> + {email} + + )} +
+ )} +
+ ); + })} +
+
+ ); +}; + +export default Section; diff --git a/client/templates/templateMap.tsx b/client/templates/templateMap.tsx index 6d829105..69294a44 100644 --- a/client/templates/templateMap.tsx +++ b/client/templates/templateMap.tsx @@ -4,6 +4,7 @@ import Castform from './Castform/Castform'; import Gengar from './Gengar/Gengar'; import Glalie from './Glalie/Glalie'; import Kakuna from './Kakuna/Kakuna'; +import Leafish from './Leafish/Leafish'; import Onyx from './Onyx/Onyx'; import Pikachu from './Pikachu/Pikachu'; @@ -51,6 +52,12 @@ const templateMap: Record = { preview: '/images/templates/glalie.jpg', component: Glalie, }, + leafish: { + id: 'leafish', + name: 'Leafish', + preview: '/images/templates/leafish.jpg', + component: Leafish, + }, }; export default templateMap;