mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-13 08:13:49 +10:00
feat(artboard): implement 8 new templates
This commit is contained in:
@ -1,10 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Artboard | Reactive Resume</title>
|
||||
<base href="/" />
|
||||
|
||||
<!-- SEO -->
|
||||
<title>Reactive Resume - A free and open-source resume builder</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume."
|
||||
/>
|
||||
|
||||
<!-- Meta -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
@ -29,6 +34,7 @@
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
||||
<!-- Phosphor Icons -->
|
||||
|
||||
@ -24,8 +24,8 @@ export const Page = ({ mode = "preview", pageNumber, children }: Props) => {
|
||||
style={{
|
||||
fontFamily,
|
||||
padding: page.margin,
|
||||
width: `${pageSizeMap[page.format].width * MM_TO_PX * window.devicePixelRatio}px`,
|
||||
minHeight: `${pageSizeMap[page.format].height * MM_TO_PX * window.devicePixelRatio}px`,
|
||||
width: `${pageSizeMap[page.format].width * MM_TO_PX}px`,
|
||||
minHeight: `${pageSizeMap[page.format].height * MM_TO_PX}px`,
|
||||
}}
|
||||
>
|
||||
{mode === "builder" && page.options.pageNumbers && (
|
||||
@ -40,7 +40,7 @@ export const Page = ({ mode = "preview", pageNumber, children }: Props) => {
|
||||
<div
|
||||
className="absolute inset-x-0 border-b border-dashed"
|
||||
style={{
|
||||
top: `${pageSizeMap[page.format].height * MM_TO_PX * window.devicePixelRatio}px`,
|
||||
top: `${pageSizeMap[page.format].height * MM_TO_PX}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { isUrl } from "@reactive-resume/utils";
|
||||
import { cn, isUrl } from "@reactive-resume/utils";
|
||||
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
|
||||
export const Picture = () => {
|
||||
type PictureProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Picture = ({ className }: PictureProps) => {
|
||||
const picture = useArtboardStore((state) => state.resume.basics.picture);
|
||||
|
||||
if (!isUrl(picture.url) || picture.effects.hidden) return null;
|
||||
@ -11,7 +15,7 @@ export const Picture = () => {
|
||||
<img
|
||||
src={picture.url}
|
||||
alt="Profile"
|
||||
className="object-cover"
|
||||
className={cn("relative z-20 object-cover", className)}
|
||||
style={{
|
||||
maxWidth: `${picture.size}px`,
|
||||
aspectRatio: `${picture.aspectRatio}`,
|
||||
|
||||
@ -30,6 +30,19 @@ export const ArtboardPage = () => {
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty("font-size", `${metadata.typography.font.size}px`);
|
||||
document.documentElement.style.setProperty("line-height", `${metadata.typography.lineHeight}`);
|
||||
|
||||
document.documentElement.style.setProperty("--font-size", `${metadata.typography.font.size}px`);
|
||||
document.documentElement.style.setProperty(
|
||||
"--line-height",
|
||||
`${metadata.typography.lineHeight}`,
|
||||
);
|
||||
|
||||
document.documentElement.style.setProperty("--color-text", `${metadata.theme.text}`);
|
||||
document.documentElement.style.setProperty("--color-primary", `${metadata.theme.primary}`);
|
||||
document.documentElement.style.setProperty(
|
||||
"--color-background",
|
||||
`${metadata.theme.background}`,
|
||||
);
|
||||
}, [metadata]);
|
||||
|
||||
// Underline Links
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
import { SectionKey } from "@reactive-resume/schema";
|
||||
import { pageSizeMap } from "@reactive-resume/utils";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { pageSizeMap, TemplateKey } from "@reactive-resume/utils";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||
|
||||
import { MM_TO_PX, Page } from "../components/page";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { Rhyhorn } from "../templates/rhyhorn";
|
||||
import { getTemplate } from "../templates";
|
||||
|
||||
export const BuilderLayout = () => {
|
||||
const transformRef = useRef<ReactZoomPanPinchRef>(null);
|
||||
const format = useArtboardStore((state) => state.resume.metadata.page.format);
|
||||
const layout = useArtboardStore((state) => state.resume.metadata.layout);
|
||||
const template = useArtboardStore((state) => state.resume.metadata.template);
|
||||
const template = useArtboardStore((state) => state.resume.metadata.template as TemplateKey);
|
||||
|
||||
const Template = useMemo(() => getTemplate(template), [template]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
@ -50,13 +53,21 @@ export const BuilderLayout = () => {
|
||||
gridTemplateColumns: `repeat(${layout.length}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{layout.map((columns, pageIndex) => (
|
||||
<Page key={pageIndex} mode="builder" pageNumber={pageIndex + 1}>
|
||||
{template === "rhyhorn" && (
|
||||
<Rhyhorn isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
|
||||
)}
|
||||
</Page>
|
||||
))}
|
||||
<AnimatePresence>
|
||||
{layout.map((columns, pageIndex) => (
|
||||
<motion.div
|
||||
layout
|
||||
key={pageIndex}
|
||||
initial={{ opacity: 0, x: -200 }}
|
||||
animate={{ opacity: 1, x: 0, transition: { delay: pageIndex * 0.3 } }}
|
||||
exit={{ opacity: 0, x: -200 }}
|
||||
>
|
||||
<Page mode="builder" pageNumber={pageIndex + 1}>
|
||||
<Template isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
|
||||
</Page>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
);
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
import { SectionKey } from "@reactive-resume/schema";
|
||||
import { TemplateKey } from "@reactive-resume/utils";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { Page } from "../components/page";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { Rhyhorn } from "../templates/rhyhorn";
|
||||
import { getTemplate } from "../templates";
|
||||
|
||||
export const PreviewLayout = () => {
|
||||
const layout = useArtboardStore((state) => state.resume.metadata.layout);
|
||||
const template = useArtboardStore((state) => state.resume.metadata.template);
|
||||
const template = useArtboardStore((state) => state.resume.metadata.template as TemplateKey);
|
||||
|
||||
const Template = useMemo(() => getTemplate(template), [template]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{layout.map((columns, pageIndex) => (
|
||||
<Page key={pageIndex} mode="preview" pageNumber={pageIndex + 1}>
|
||||
{template === "rhyhorn" && (
|
||||
<Rhyhorn isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
|
||||
)}
|
||||
<Template isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
|
||||
</Page>
|
||||
))}
|
||||
</>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { sampleResume } from "@reactive-resume/schema";
|
||||
import { useEffect } from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
@ -30,9 +31,9 @@ export const Providers = () => {
|
||||
}, [setResume]);
|
||||
|
||||
// Only for testing, in production this will be fetched from window.postMessage
|
||||
// useEffect(() => {
|
||||
// setResume(sampleResume);
|
||||
// }, [setResume]);
|
||||
useEffect(() => {
|
||||
setResume(sampleResume);
|
||||
}, [setResume]);
|
||||
|
||||
if (!resume) return null;
|
||||
|
||||
|
||||
@ -15,5 +15,5 @@
|
||||
}
|
||||
|
||||
.wysiwyg {
|
||||
@apply prose max-w-none text-current prose-headings:my-1.5 prose-p:my-1.5 prose-ul:my-1.5 prose-li:my-1.5 prose-ol:my-1.5 prose-img:my-1.5 prose-hr:my-1.5;
|
||||
@apply prose max-w-none text-current prose-headings:mt-0 prose-headings:mb-2 prose-p:mt-0 prose-p:mb-2 prose-ul:mt-0 prose-ul:mb-2 prose-li:mt-0 prose-li:mb-2 prose-ol:mt-0 prose-ol:mb-2 prose-img:mt-0 prose-img:mb-2 prose-hr:mt-0 prose-hr:mb-2 prose-p:leading-normal;
|
||||
}
|
||||
|
||||
506
apps/artboard/src/templates/azurill.tsx
Normal file
506
apps/artboard/src/templates/azurill.tsx
Normal file
@ -0,0 +1,506 @@
|
||||
import {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
Project,
|
||||
Publication,
|
||||
Reference,
|
||||
SectionKey,
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl, linearTransform } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-2 pb-2 text-center">
|
||||
<Picture />
|
||||
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{basics.name}</div>
|
||||
<div className="text-base">{basics.headline}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-sm">
|
||||
{basics.location && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-map-pin text-primary" />
|
||||
<div>{basics.location}</div>
|
||||
</div>
|
||||
)}
|
||||
{basics.phone && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-phone text-primary" />
|
||||
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
|
||||
{basics.phone}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{basics.email && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-at text-primary" />
|
||||
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
|
||||
{basics.email}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<Link url={basics.url} />
|
||||
{basics.customFields.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-x-1.5">
|
||||
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
|
||||
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Summary = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.summary);
|
||||
|
||||
if (!section.visible || isEmptyString(section.content)) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<div className="mb-2 hidden font-bold uppercase text-primary group-[.main]:block">
|
||||
<h4>{section.name}</h4>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 hidden items-center gap-x-2 text-center font-bold uppercase text-primary group-[.sidebar]:flex">
|
||||
<div className="h-1.5 w-1.5 rounded-full border border-primary" />
|
||||
<h4>{section.name}</h4>
|
||||
<div className="h-1.5 w-1.5 rounded-full border border-primary" />
|
||||
</div>
|
||||
|
||||
<main className={cn("relative space-y-2", "border-l border-primary pl-4")}>
|
||||
<div className="absolute left-[-4.5px] top-[8px] hidden h-[8px] w-[8px] rounded-full bg-primary group-[.main]:block" />
|
||||
|
||||
<div
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</main>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
type RatingProps = { level: number };
|
||||
|
||||
const Rating = ({ level }: RatingProps) => (
|
||||
<div className="relative h-1 w-[128px] group-[.sidebar]:mx-auto">
|
||||
<div className="absolute inset-0 h-1 w-[128px] rounded bg-primary opacity-25" />
|
||||
<div
|
||||
className="absolute inset-0 h-1 rounded bg-primary"
|
||||
style={{ width: linearTransform(level, 0, 5, 0, 128) }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
type LinkProps = {
|
||||
url: URL;
|
||||
icon?: React.ReactNode;
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SectionProps<T> = {
|
||||
section: SectionWithItem<T> | CustomSectionGroup;
|
||||
children?: (item: T) => React.ReactNode;
|
||||
className?: string;
|
||||
urlKey?: keyof T;
|
||||
levelKey?: keyof T;
|
||||
summaryKey?: keyof T;
|
||||
keywordsKey?: keyof T;
|
||||
};
|
||||
|
||||
const Section = <T,>({
|
||||
section,
|
||||
children,
|
||||
className,
|
||||
urlKey,
|
||||
levelKey,
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
<div className="mb-2 hidden font-bold uppercase text-primary group-[.main]:block">
|
||||
<h4>{section.name}</h4>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mb-2 hidden items-center gap-x-2 text-center font-bold uppercase text-primary group-[.sidebar]:flex">
|
||||
<div className="h-1.5 w-1.5 rounded-full border border-primary" />
|
||||
<h4>{section.name}</h4>
|
||||
<div className="h-1.5 w-1.5 rounded-full border border-primary" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="grid gap-3 group-[.sidebar]:mx-auto group-[.sidebar]:text-center"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items
|
||||
.filter((item) => item.visible)
|
||||
.map((item) => {
|
||||
const url = (urlKey && get(item, urlKey)) as URL | undefined;
|
||||
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
|
||||
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
|
||||
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"relative space-y-2",
|
||||
"border-primary group-[.main]:border-l group-[.main]:pl-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="leading-snug">{children?.(item as T)}</div>
|
||||
|
||||
{summary && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
)}
|
||||
|
||||
{level && level > 0 && <Rating level={level} />}
|
||||
|
||||
{keywords && keywords.length > 0 && (
|
||||
<p className="text-sm">{keywords.join(", ")}</p>
|
||||
)}
|
||||
|
||||
{url && <Link url={url} />}
|
||||
|
||||
<div className="absolute left-[-4.5px] top-px hidden h-[8px] w-[8px] rounded-full bg-primary group-[.main]:block" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Profiles = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
||||
|
||||
return (
|
||||
<Section<Profile> section={section}>
|
||||
{(item) => (
|
||||
<div className="leading-snug">
|
||||
{isUrl(item.url.href) ? (
|
||||
<Link
|
||||
url={item.url}
|
||||
label={item.username}
|
||||
icon={
|
||||
<img
|
||||
width={fontSize}
|
||||
height={fontSize}
|
||||
alt={item.network}
|
||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<p>{item.username}</p>
|
||||
)}
|
||||
<p className="text-sm">{item.network}</p>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Experience = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.experience);
|
||||
|
||||
return (
|
||||
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.company}</div>
|
||||
<div>{item.position}</div>
|
||||
<div>{item.location}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Education = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.education);
|
||||
|
||||
return (
|
||||
<Section<Education> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.institution}</div>
|
||||
<div>{item.area}</div>
|
||||
<div>{item.score}</div>
|
||||
<div>{item.studyType}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Awards = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.awards);
|
||||
|
||||
return (
|
||||
<Section<Award> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.title}</div>
|
||||
<div>{item.awarder}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Certifications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.certifications);
|
||||
|
||||
return (
|
||||
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.issuer}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Skills = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.skills);
|
||||
|
||||
return (
|
||||
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Interests = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.interests);
|
||||
|
||||
return (
|
||||
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
|
||||
{(item) => <div className="font-bold">{item.name}</div>}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Publications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.publications);
|
||||
|
||||
return (
|
||||
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.publisher}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Volunteer = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.volunteer);
|
||||
|
||||
return (
|
||||
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.organization}</div>
|
||||
<div>{item.position}</div>
|
||||
<div>{item.location}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Languages = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.languages);
|
||||
|
||||
return (
|
||||
<Section<Language> section={section} levelKey="fluencyLevel">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.fluency}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Projects = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.projects);
|
||||
|
||||
return (
|
||||
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const References = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.references);
|
||||
|
||||
return (
|
||||
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Custom = ({ id }: { id: string }) => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
|
||||
|
||||
return (
|
||||
<Section<CustomSection>
|
||||
section={section}
|
||||
urlKey="url"
|
||||
summaryKey="summary"
|
||||
keywordsKey="keywords"
|
||||
>
|
||||
{(item) => (
|
||||
<div>
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
return <Experience />;
|
||||
case "education":
|
||||
return <Education />;
|
||||
case "awards":
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
return <Projects />;
|
||||
case "references":
|
||||
return <References />;
|
||||
default:
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const Azurill = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="grid grid-cols-3 gap-x-4">
|
||||
<div className="sidebar group col-span-1 space-y-4">
|
||||
{sidebar.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="main group col-span-2 space-y-4">
|
||||
{main.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
513
apps/artboard/src/templates/bronzor.tsx
Normal file
513
apps/artboard/src/templates/bronzor.tsx
Normal file
@ -0,0 +1,513 @@
|
||||
import {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
Project,
|
||||
Publication,
|
||||
Reference,
|
||||
SectionKey,
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-2 text-center">
|
||||
<Picture />
|
||||
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{basics.name}</div>
|
||||
<div className="text-base">{basics.headline}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
|
||||
{basics.location && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-map-pin text-primary" />
|
||||
<div>{basics.location}</div>
|
||||
</div>
|
||||
)}
|
||||
{basics.phone && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-phone text-primary" />
|
||||
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
|
||||
{basics.phone}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{basics.email && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-at text-primary" />
|
||||
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
|
||||
{basics.email}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<Link url={basics.url} />
|
||||
{basics.customFields.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-x-1.5">
|
||||
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
|
||||
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Summary = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.summary);
|
||||
|
||||
if (!section.visible || isEmptyString(section.content)) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid grid-cols-5 border-t pt-2.5">
|
||||
<div className="col-span-1">
|
||||
<h4 className="text-base font-bold">{section.name}</h4>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="wysiwyg col-span-4"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
type RatingProps = { level: number };
|
||||
|
||||
const Rating = ({ level }: RatingProps) => (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn("h-2 w-2 rounded-full border border-primary", level > index && "bg-primary")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
type LinkProps = {
|
||||
url: URL;
|
||||
icon?: React.ReactNode;
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SectionProps<T> = {
|
||||
section: SectionWithItem<T> | CustomSectionGroup;
|
||||
children?: (item: T) => React.ReactNode;
|
||||
className?: string;
|
||||
urlKey?: keyof T;
|
||||
levelKey?: keyof T;
|
||||
summaryKey?: keyof T;
|
||||
keywordsKey?: keyof T;
|
||||
};
|
||||
|
||||
const Section = <T,>({
|
||||
section,
|
||||
children,
|
||||
className,
|
||||
urlKey,
|
||||
levelKey,
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid grid-cols-5 border-t pt-2.5">
|
||||
<div className="col-span-1">
|
||||
<h4 className="text-base font-bold">{section.name}</h4>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="col-span-4 grid gap-3"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items
|
||||
.filter((item) => item.visible)
|
||||
.map((item) => {
|
||||
const url = (urlKey && get(item, urlKey)) as URL | undefined;
|
||||
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
|
||||
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
|
||||
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
|
||||
|
||||
return (
|
||||
<div key={item.id} className={cn("space-y-2", className)}>
|
||||
<div className="leading-snug">
|
||||
{children?.(item as T)}
|
||||
{url && <Link url={url} />}
|
||||
</div>
|
||||
|
||||
{summary && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
)}
|
||||
|
||||
{level && level > 0 && <Rating level={level} />}
|
||||
|
||||
{keywords && keywords.length > 0 && (
|
||||
<p className="text-sm">{keywords.join(", ")}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Profiles = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
||||
|
||||
return (
|
||||
<Section<Profile> section={section}>
|
||||
{(item) => (
|
||||
<div className="leading-snug">
|
||||
{isUrl(item.url.href) ? (
|
||||
<Link
|
||||
url={item.url}
|
||||
label={item.username}
|
||||
icon={
|
||||
<img
|
||||
width={fontSize}
|
||||
height={fontSize}
|
||||
alt={item.network}
|
||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<p>{item.username}</p>
|
||||
)}
|
||||
<p className="text-sm">{item.network}</p>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Experience = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.experience);
|
||||
|
||||
return (
|
||||
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.company}</div>
|
||||
<div>{item.position}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Education = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.education);
|
||||
|
||||
return (
|
||||
<Section<Education> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.institution}</div>
|
||||
<div>{item.area}</div>
|
||||
<div>{item.score}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.studyType}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Awards = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.awards);
|
||||
|
||||
return (
|
||||
<Section<Award> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.title}</div>
|
||||
<div>{item.awarder}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Certifications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.certifications);
|
||||
|
||||
return (
|
||||
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.issuer}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Skills = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.skills);
|
||||
|
||||
return (
|
||||
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Interests = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.interests);
|
||||
|
||||
return (
|
||||
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
|
||||
{(item) => <div className="font-bold">{item.name}</div>}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Publications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.publications);
|
||||
|
||||
return (
|
||||
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.publisher}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Volunteer = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.volunteer);
|
||||
|
||||
return (
|
||||
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.organization}</div>
|
||||
<div>{item.position}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Languages = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.languages);
|
||||
|
||||
return (
|
||||
<Section<Language> section={section} levelKey="fluencyLevel">
|
||||
{(item) => (
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.fluency}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Projects = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.projects);
|
||||
|
||||
return (
|
||||
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const References = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.references);
|
||||
|
||||
return (
|
||||
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Custom = ({ id }: { id: string }) => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
|
||||
|
||||
return (
|
||||
<Section<CustomSection>
|
||||
section={section}
|
||||
urlKey="url"
|
||||
summaryKey="summary"
|
||||
keywordsKey="keywords"
|
||||
>
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
return <Experience />;
|
||||
case "education":
|
||||
return <Education />;
|
||||
case "awards":
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
return <Projects />;
|
||||
case "references":
|
||||
return <References />;
|
||||
default:
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const Bronzor = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="space-y-4">
|
||||
{main.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
|
||||
{sidebar.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
516
apps/artboard/src/templates/chikorita.tsx
Normal file
516
apps/artboard/src/templates/chikorita.tsx
Normal file
@ -0,0 +1,516 @@
|
||||
import {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Project,
|
||||
Publication,
|
||||
Reference,
|
||||
SectionKey,
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3">
|
||||
<div className="col-span-2 flex items-center gap-x-4">
|
||||
<Picture />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{basics.name}</div>
|
||||
<div className="text-base">{basics.headline}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
|
||||
{basics.location && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-map-pin text-primary" />
|
||||
<div>{basics.location}</div>
|
||||
</div>
|
||||
)}
|
||||
{basics.phone && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-phone text-primary" />
|
||||
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
|
||||
{basics.phone}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{basics.email && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-at text-primary" />
|
||||
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
|
||||
{basics.email}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<Link url={basics.url} />
|
||||
{basics.customFields.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-x-1.5">
|
||||
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
|
||||
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{profiles.visible && profiles.items.length > 0 && (
|
||||
<div className="flex items-center gap-x-3 gap-y-0.5">
|
||||
{profiles.items
|
||||
.filter((item) => item.visible)
|
||||
.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-x-2">
|
||||
<Link
|
||||
url={item.url}
|
||||
label={item.username}
|
||||
className="text-sm"
|
||||
icon={
|
||||
<img
|
||||
width="12"
|
||||
height="12"
|
||||
alt={item.network}
|
||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Summary = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.summary);
|
||||
|
||||
if (!section.visible || isEmptyString(section.content)) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold uppercase">{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
type RatingProps = { level: number };
|
||||
|
||||
const Rating = ({ level }: RatingProps) => (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full border border-primary group-[.sidebar]:border-background",
|
||||
level > index && "bg-primary group-[.sidebar]:bg-background",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
type LinkProps = {
|
||||
url: URL;
|
||||
icon?: React.ReactNode;
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SectionProps<T> = {
|
||||
section: SectionWithItem<T> | CustomSectionGroup;
|
||||
children?: (item: T) => React.ReactNode;
|
||||
className?: string;
|
||||
urlKey?: keyof T;
|
||||
levelKey?: keyof T;
|
||||
summaryKey?: keyof T;
|
||||
keywordsKey?: keyof T;
|
||||
};
|
||||
|
||||
const Section = <T,>({
|
||||
section,
|
||||
children,
|
||||
className,
|
||||
urlKey,
|
||||
levelKey,
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold uppercase">{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid gap-3"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items
|
||||
.filter((item) => item.visible)
|
||||
.map((item) => {
|
||||
const url = (urlKey && get(item, urlKey)) as URL | undefined;
|
||||
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
|
||||
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
|
||||
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
|
||||
|
||||
return (
|
||||
<div key={item.id} className={cn("space-y-2", className)}>
|
||||
<div className="leading-snug">
|
||||
{children?.(item as T)}
|
||||
{url && <Link url={url} />}
|
||||
</div>
|
||||
|
||||
{summary && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
)}
|
||||
|
||||
{level && level > 0 && <Rating level={level} />}
|
||||
|
||||
{keywords && keywords.length > 0 && (
|
||||
<p className="text-sm">{keywords.join(", ")}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Experience = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.experience);
|
||||
|
||||
return (
|
||||
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.company}</div>
|
||||
<div>{item.position}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Education = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.education);
|
||||
|
||||
return (
|
||||
<Section<Education> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.institution}</div>
|
||||
<div>{item.area}</div>
|
||||
<div>{item.score}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.studyType}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Awards = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.awards);
|
||||
|
||||
return (
|
||||
<Section<Award> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.title}</div>
|
||||
<div>{item.awarder}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Certifications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.certifications);
|
||||
|
||||
return (
|
||||
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.issuer}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Skills = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.skills);
|
||||
|
||||
return (
|
||||
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Interests = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.interests);
|
||||
|
||||
return (
|
||||
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
|
||||
{(item) => <div className="font-bold">{item.name}</div>}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Publications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.publications);
|
||||
|
||||
return (
|
||||
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.publisher}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Volunteer = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.volunteer);
|
||||
|
||||
return (
|
||||
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.organization}</div>
|
||||
<div>{item.position}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Languages = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.languages);
|
||||
|
||||
return (
|
||||
<Section<Language> section={section} levelKey="fluencyLevel">
|
||||
{(item) => (
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.fluency}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Projects = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.projects);
|
||||
|
||||
return (
|
||||
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const References = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.references);
|
||||
|
||||
return (
|
||||
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Custom = ({ id }: { id: string }) => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
|
||||
|
||||
return (
|
||||
<Section<CustomSection>
|
||||
section={section}
|
||||
urlKey="url"
|
||||
summaryKey="summary"
|
||||
keywordsKey="keywords"
|
||||
>
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "summary":
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
return <Experience />;
|
||||
case "education":
|
||||
return <Education />;
|
||||
case "awards":
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
return <Projects />;
|
||||
case "references":
|
||||
return <References />;
|
||||
default:
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const margin = useArtboardStore((state) => state.resume.metadata.page.margin);
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="relative z-10 grid grid-cols-3 space-x-8">
|
||||
<div className="main group col-span-2 space-y-4">
|
||||
{main.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="sidebar group col-span-1 space-y-4 text-background">
|
||||
{sidebar.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 grid grid-cols-3" style={{ top: -margin }}>
|
||||
<div className="col-span-1 col-start-3 ml-2 bg-primary"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
546
apps/artboard/src/templates/ditto.tsx
Normal file
546
apps/artboard/src/templates/ditto.tsx
Normal file
@ -0,0 +1,546 @@
|
||||
import {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
Project,
|
||||
Publication,
|
||||
Reference,
|
||||
SectionKey,
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
|
||||
return (
|
||||
<div className="relative z-20 grid grid-cols-3 space-x-4">
|
||||
<div className="col-span-1 mx-auto">
|
||||
<Picture />
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 space-y-0.5 text-background">
|
||||
<h2 className="min-h-[30px] text-4xl font-bold">{basics.name}</h2>
|
||||
<p className="min-h-[24px]">{basics.headline}</p>
|
||||
|
||||
<div className="text-text !mt-10">
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
|
||||
{basics.location && (
|
||||
<>
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-map-pin text-primary" />
|
||||
<div>{basics.location}</div>
|
||||
</div>
|
||||
<div className="bg-text h-1 w-1 rounded-full last:hidden" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{basics.phone && (
|
||||
<>
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-phone text-primary" />
|
||||
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
|
||||
{basics.phone}
|
||||
</a>
|
||||
</div>
|
||||
<div className="bg-text h-1 w-1 rounded-full last:hidden" />
|
||||
</>
|
||||
)}
|
||||
{basics.email && (
|
||||
<>
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-at text-primary" />
|
||||
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
|
||||
{basics.email}
|
||||
</a>
|
||||
</div>
|
||||
<div className="bg-text h-1 w-1 rounded-full last:hidden" />
|
||||
</>
|
||||
)}
|
||||
{isUrl(basics.url.href) && (
|
||||
<>
|
||||
<Link url={basics.url} />
|
||||
<div className="bg-text h-1 w-1 rounded-full last:hidden" />
|
||||
</>
|
||||
)}
|
||||
{basics.customFields.map((item) => (
|
||||
<>
|
||||
<div key={item.id} className="flex items-center gap-x-1.5">
|
||||
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
|
||||
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
|
||||
</div>
|
||||
<div className="bg-text h-1 w-1 rounded-full last:hidden" />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Summary = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.summary);
|
||||
|
||||
if (!section.visible || isEmptyString(section.content)) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className="mb-2 text-base font-bold uppercase">{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
type RatingProps = { level: number };
|
||||
|
||||
const Rating = ({ level }: RatingProps) => (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn("h-2 w-4 border border-primary", level > index && "bg-primary")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
type LinkProps = {
|
||||
url: URL;
|
||||
icon?: React.ReactNode;
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SectionProps<T> = {
|
||||
section: SectionWithItem<T> | CustomSectionGroup;
|
||||
children?: (item: T) => React.ReactNode;
|
||||
className?: string;
|
||||
urlKey?: keyof T;
|
||||
levelKey?: keyof T;
|
||||
summaryKey?: keyof T;
|
||||
keywordsKey?: keyof T;
|
||||
};
|
||||
|
||||
const Section = <T,>({
|
||||
section,
|
||||
children,
|
||||
className,
|
||||
urlKey,
|
||||
levelKey,
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
<h4 className="mb-2 text-base font-bold uppercase">{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid gap-3"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items
|
||||
.filter((item) => item.visible)
|
||||
.map((item) => {
|
||||
const url = (urlKey && get(item, urlKey)) as URL | undefined;
|
||||
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
|
||||
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
|
||||
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn("relative space-y-2 pl-4 group-[.sidebar]:pl-0", className)}
|
||||
>
|
||||
<div className="relative -ml-4 group-[.sidebar]:ml-0">
|
||||
<div className="pl-4 leading-snug group-[.sidebar]:pl-0">
|
||||
{children?.(item as T)}
|
||||
{url && <Link url={url} />}
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-y-0 -left-px border-l-[4px] border-primary group-[.sidebar]:hidden" />
|
||||
</div>
|
||||
|
||||
{summary && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
)}
|
||||
|
||||
{level && level > 0 && <Rating level={level} />}
|
||||
|
||||
{keywords && keywords.length > 0 && (
|
||||
<p className="text-sm">{keywords.join(", ")}</p>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-y-0 left-0 border-l border-primary group-[.sidebar]:hidden" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Profiles = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
||||
|
||||
return (
|
||||
<Section<Profile> section={section}>
|
||||
{(item) => (
|
||||
<div className="leading-snug">
|
||||
{isUrl(item.url.href) ? (
|
||||
<Link
|
||||
url={item.url}
|
||||
label={item.username}
|
||||
icon={
|
||||
<img
|
||||
width={fontSize}
|
||||
height={fontSize}
|
||||
alt={item.network}
|
||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<p>{item.username}</p>
|
||||
)}
|
||||
<p className="text-sm">{item.network}</p>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Experience = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.experience);
|
||||
|
||||
return (
|
||||
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.company}</div>
|
||||
<div>{item.position}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Education = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.education);
|
||||
|
||||
return (
|
||||
<Section<Education> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.institution}</div>
|
||||
<div>{item.area}</div>
|
||||
<div>{item.score}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.studyType}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Awards = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.awards);
|
||||
|
||||
return (
|
||||
<Section<Award> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.title}</div>
|
||||
<div>{item.awarder}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Certifications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.certifications);
|
||||
|
||||
return (
|
||||
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.issuer}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Skills = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.skills);
|
||||
|
||||
return (
|
||||
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Interests = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.interests);
|
||||
|
||||
return (
|
||||
<Section<Interest> section={section} className="space-y-0" keywordsKey="keywords">
|
||||
{(item) => <div className="font-bold">{item.name}</div>}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Publications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.publications);
|
||||
|
||||
return (
|
||||
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.publisher}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Volunteer = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.volunteer);
|
||||
|
||||
return (
|
||||
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.organization}</div>
|
||||
<div>{item.position}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Languages = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.languages);
|
||||
|
||||
return (
|
||||
<Section<Language> section={section} levelKey="fluencyLevel">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.fluency}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Projects = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.projects);
|
||||
|
||||
return (
|
||||
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const References = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.references);
|
||||
|
||||
return (
|
||||
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Custom = ({ id }: { id: string }) => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
|
||||
|
||||
return (
|
||||
<Section<CustomSection>
|
||||
section={section}
|
||||
urlKey="url"
|
||||
summaryKey="summary"
|
||||
keywordsKey="keywords"
|
||||
>
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
return <Experience />;
|
||||
case "education":
|
||||
return <Education />;
|
||||
case "awards":
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
return <Projects />;
|
||||
case "references":
|
||||
return <References />;
|
||||
default:
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{isFirstPage && <div className="absolute inset-x-0 top-0 z-10 h-32 bg-primary" />}
|
||||
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="grid grid-cols-3 space-x-4">
|
||||
<div className="sidebar group col-span-1 space-y-4">
|
||||
{sidebar.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="main group col-span-2 space-y-4">
|
||||
{main.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
33
apps/artboard/src/templates/index.tsx
Normal file
33
apps/artboard/src/templates/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { TemplateKey } from "@reactive-resume/utils";
|
||||
|
||||
import { Azurill } from "./azurill";
|
||||
import { Bronzor } from "./bronzor";
|
||||
import { Chikorita } from "./chikorita";
|
||||
import { Ditto } from "./ditto";
|
||||
import { Kakuna } from "./kakuna";
|
||||
import { Onyx } from "./onyx";
|
||||
import { Pikachu } from "./pikachu";
|
||||
import { Rhyhorn } from "./rhyhorn";
|
||||
|
||||
export const getTemplate = (template: TemplateKey) => {
|
||||
switch (template) {
|
||||
case "onyx":
|
||||
return Onyx;
|
||||
case "kakuna":
|
||||
return Kakuna;
|
||||
case "rhyhorn":
|
||||
return Rhyhorn;
|
||||
case "azurill":
|
||||
return Azurill;
|
||||
case "ditto":
|
||||
return Ditto;
|
||||
case "chikorita":
|
||||
return Chikorita;
|
||||
case "bronzor":
|
||||
return Bronzor;
|
||||
case "pikachu":
|
||||
return Pikachu;
|
||||
default:
|
||||
return Onyx;
|
||||
}
|
||||
};
|
||||
467
apps/artboard/src/templates/kakuna.tsx
Normal file
467
apps/artboard/src/templates/kakuna.tsx
Normal file
@ -0,0 +1,467 @@
|
||||
import {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Project,
|
||||
Publication,
|
||||
Reference,
|
||||
SectionKey,
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-2 pb-2 text-center">
|
||||
<Picture />
|
||||
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{basics.name}</div>
|
||||
<div className="text-base">{basics.headline}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-sm">
|
||||
{basics.location && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-map-pin text-primary" />
|
||||
<div>{basics.location}</div>
|
||||
</div>
|
||||
)}
|
||||
{basics.phone && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-phone text-primary" />
|
||||
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
|
||||
{basics.phone}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{basics.email && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-at text-primary" />
|
||||
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
|
||||
{basics.email}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<Link url={basics.url} />
|
||||
{basics.customFields.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-x-1.5">
|
||||
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
|
||||
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{profiles.visible && profiles.items.length > 0 && (
|
||||
<div className="flex items-center gap-x-3 gap-y-0.5">
|
||||
{profiles.items
|
||||
.filter((item) => item.visible)
|
||||
.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-x-2">
|
||||
<Link
|
||||
url={item.url}
|
||||
label={item.username}
|
||||
className="text-sm"
|
||||
icon={
|
||||
<img
|
||||
width="12"
|
||||
height="12"
|
||||
alt={item.network}
|
||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Summary = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.summary);
|
||||
|
||||
if (!section.visible || isEmptyString(section.content)) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className="mb-2 border-b border-primary text-center font-bold uppercase text-primary">
|
||||
{section.name}
|
||||
</h4>
|
||||
|
||||
<div
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
type RatingProps = { level: number };
|
||||
|
||||
const Rating = ({ level }: RatingProps) => (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn("h-3 w-5 rounded border-2 border-primary", level > index && "bg-primary")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
type LinkProps = {
|
||||
url: URL;
|
||||
icon?: React.ReactNode;
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SectionProps<T> = {
|
||||
section: SectionWithItem<T> | CustomSectionGroup;
|
||||
children?: (item: T) => React.ReactNode;
|
||||
className?: string;
|
||||
urlKey?: keyof T;
|
||||
levelKey?: keyof T;
|
||||
summaryKey?: keyof T;
|
||||
keywordsKey?: keyof T;
|
||||
};
|
||||
|
||||
const Section = <T,>({
|
||||
section,
|
||||
children,
|
||||
className,
|
||||
urlKey,
|
||||
levelKey,
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
<h4 className="mb-2 border-b border-primary text-center font-bold uppercase text-primary">
|
||||
{section.name}
|
||||
</h4>
|
||||
|
||||
<div
|
||||
className="grid gap-3"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items
|
||||
.filter((item) => item.visible)
|
||||
.map((item) => {
|
||||
const url = (urlKey && get(item, urlKey)) as URL | undefined;
|
||||
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
|
||||
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
|
||||
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
|
||||
|
||||
return (
|
||||
<div key={item.id} className={cn("space-y-2", className)}>
|
||||
<div className="leading-snug">{children?.(item as T)}</div>
|
||||
|
||||
{summary && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
)}
|
||||
|
||||
{level && level > 0 && <Rating level={level} />}
|
||||
|
||||
{keywords && keywords.length > 0 && (
|
||||
<p className="text-sm">{keywords.join(", ")}</p>
|
||||
)}
|
||||
|
||||
{url && <Link url={url} />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Experience = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.experience);
|
||||
|
||||
return (
|
||||
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.company}</div>
|
||||
<div>{item.position}</div>
|
||||
<div>{item.location}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Education = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.education);
|
||||
|
||||
return (
|
||||
<Section<Education> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.institution}</div>
|
||||
<div>{item.area}</div>
|
||||
<div>{item.score}</div>
|
||||
<div>{item.studyType}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Awards = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.awards);
|
||||
|
||||
return (
|
||||
<Section<Award> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.title}</div>
|
||||
<div>{item.awarder}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Certifications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.certifications);
|
||||
|
||||
return (
|
||||
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.issuer}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Skills = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.skills);
|
||||
|
||||
return (
|
||||
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Interests = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.interests);
|
||||
|
||||
return (
|
||||
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
|
||||
{(item) => <div className="font-bold">{item.name}</div>}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Publications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.publications);
|
||||
|
||||
return (
|
||||
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.publisher}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Volunteer = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.volunteer);
|
||||
|
||||
return (
|
||||
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.organization}</div>
|
||||
<div>{item.position}</div>
|
||||
<div>{item.location}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Languages = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.languages);
|
||||
|
||||
return (
|
||||
<Section<Language> section={section} levelKey="fluencyLevel">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.fluency}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Projects = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.projects);
|
||||
|
||||
return (
|
||||
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const References = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.references);
|
||||
|
||||
return (
|
||||
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Custom = ({ id }: { id: string }) => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
|
||||
|
||||
return (
|
||||
<Section<CustomSection>
|
||||
section={section}
|
||||
urlKey="url"
|
||||
summaryKey="summary"
|
||||
keywordsKey="keywords"
|
||||
>
|
||||
{(item) => (
|
||||
<div>
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
<div>{item.location}</div>
|
||||
<div className="font-bold italic">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "summary":
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
return <Experience />;
|
||||
case "education":
|
||||
return <Education />;
|
||||
case "awards":
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
return <Projects />;
|
||||
case "references":
|
||||
return <References />;
|
||||
default:
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const Kakuna = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="space-y-4">
|
||||
{main.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
|
||||
{sidebar.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
505
apps/artboard/src/templates/onyx.tsx
Normal file
505
apps/artboard/src/templates/onyx.tsx
Normal file
@ -0,0 +1,505 @@
|
||||
import {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Project,
|
||||
Publication,
|
||||
Reference,
|
||||
SectionKey,
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import React, { Fragment } from "react";
|
||||
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between space-x-4 border-b border-primary pb-5">
|
||||
<Picture />
|
||||
|
||||
<div className="flex-1 space-y-2">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{basics.name}</div>
|
||||
<div className="text-base">{basics.headline}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
|
||||
{basics.location && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-map-pin text-primary" />
|
||||
<div>{basics.location}</div>
|
||||
</div>
|
||||
)}
|
||||
{basics.phone && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-phone text-primary" />
|
||||
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
|
||||
{basics.phone}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{basics.email && (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-at text-primary" />
|
||||
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
|
||||
{basics.email}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<Link url={basics.url} />
|
||||
{basics.customFields.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-x-1.5">
|
||||
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
|
||||
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{profiles.visible && profiles.items.length > 0 && (
|
||||
<div
|
||||
className="grid gap-x-4 gap-y-1 self-end text-right"
|
||||
style={{ gridTemplateColumns: `repeat(${profiles.columns}, auto)` }}
|
||||
>
|
||||
{profiles.items
|
||||
.filter((item) => item.visible)
|
||||
.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-x-2">
|
||||
<Link
|
||||
url={item.url}
|
||||
label={item.username}
|
||||
className="text-sm"
|
||||
icon={
|
||||
<img
|
||||
width="12"
|
||||
height="12"
|
||||
alt={item.network}
|
||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Summary = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.summary);
|
||||
|
||||
if (!section.visible || isEmptyString(section.content)) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className="mb-1 font-bold uppercase text-primary">{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
type RatingProps = { level: number };
|
||||
|
||||
const Rating = ({ level }: RatingProps) => (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn("h-3 w-3 rounded border-2 border-primary", level > index && "bg-primary")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
type LinkProps = {
|
||||
url: URL;
|
||||
icon?: React.ReactNode;
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SectionProps<T> = {
|
||||
section: SectionWithItem<T> | CustomSectionGroup;
|
||||
children?: (item: T) => React.ReactNode;
|
||||
className?: string;
|
||||
urlKey?: keyof T;
|
||||
levelKey?: keyof T;
|
||||
summaryKey?: keyof T;
|
||||
keywordsKey?: keyof T;
|
||||
};
|
||||
|
||||
const Section = <T,>({
|
||||
section,
|
||||
children,
|
||||
className,
|
||||
urlKey,
|
||||
levelKey,
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
<h4 className="mb-1 font-bold uppercase text-primary">{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid gap-3"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items
|
||||
.filter((item) => item.visible)
|
||||
.map((item) => {
|
||||
const url = (urlKey && get(item, urlKey)) as URL | undefined;
|
||||
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
|
||||
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
|
||||
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
|
||||
|
||||
return (
|
||||
<div key={item.id} className={cn("space-y-2", className)}>
|
||||
<div className="leading-snug">
|
||||
{children?.(item as T)}
|
||||
{url && <Link url={url} />}
|
||||
</div>
|
||||
|
||||
{summary && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
)}
|
||||
|
||||
{level && level > 0 && <Rating level={level} />}
|
||||
|
||||
{keywords && keywords.length > 0 && (
|
||||
<p className="text-sm">{keywords.join(", ")}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Experience = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.experience);
|
||||
|
||||
return (
|
||||
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.company}</div>
|
||||
<div>{item.position}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Education = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.education);
|
||||
|
||||
return (
|
||||
<Section<Education> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.institution}</div>
|
||||
<div>{item.area}</div>
|
||||
<div>{item.score}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.studyType}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Awards = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.awards);
|
||||
|
||||
return (
|
||||
<Section<Award> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.title}</div>
|
||||
<div>{item.awarder}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Certifications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.certifications);
|
||||
|
||||
return (
|
||||
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.issuer}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Skills = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.skills);
|
||||
|
||||
return (
|
||||
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Interests = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.interests);
|
||||
|
||||
return (
|
||||
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
|
||||
{(item) => <div className="font-bold">{item.name}</div>}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Publications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.publications);
|
||||
|
||||
return (
|
||||
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.publisher}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Volunteer = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.volunteer);
|
||||
|
||||
return (
|
||||
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.organization}</div>
|
||||
<div>{item.position}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Languages = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.languages);
|
||||
|
||||
return (
|
||||
<Section<Language> section={section} levelKey="fluencyLevel">
|
||||
{(item) => (
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.fluency}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Projects = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.projects);
|
||||
|
||||
return (
|
||||
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const References = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.references);
|
||||
|
||||
return (
|
||||
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Custom = ({ id }: { id: string }) => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
|
||||
|
||||
return (
|
||||
<Section<CustomSection>
|
||||
section={section}
|
||||
urlKey="url"
|
||||
summaryKey="summary"
|
||||
keywordsKey="keywords"
|
||||
>
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "summary":
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
return <Experience />;
|
||||
case "education":
|
||||
return <Education />;
|
||||
case "awards":
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
return <Projects />;
|
||||
case "references":
|
||||
return <References />;
|
||||
default:
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const Onyx = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="space-y-4">
|
||||
{main.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
|
||||
{sidebar.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
546
apps/artboard/src/templates/pikachu.tsx
Normal file
546
apps/artboard/src/templates/pikachu.tsx
Normal file
@ -0,0 +1,546 @@
|
||||
import {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
Project,
|
||||
Publication,
|
||||
Reference,
|
||||
SectionKey,
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
const borderRadius = useArtboardStore((state) => state.resume.basics.picture.borderRadius);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="summary group bg-primary px-6 pb-7 pt-6 text-background"
|
||||
style={{ borderRadius: `calc(${borderRadius}px - 2px)` }}
|
||||
>
|
||||
<div className="col-span-2 space-y-4">
|
||||
<div className="leading-tight">
|
||||
<h2 className="text-2xl font-bold">{basics.name}</h2>
|
||||
<p>{basics.headline}</p>
|
||||
</div>
|
||||
|
||||
<hr className="border-background opacity-50" />
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
|
||||
{basics.location && (
|
||||
<>
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-map-pin" />
|
||||
<div>{basics.location}</div>
|
||||
</div>
|
||||
<div className="h-1 w-1 rounded-full bg-background last:hidden" />
|
||||
</>
|
||||
)}
|
||||
{basics.phone && (
|
||||
<>
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-phone" />
|
||||
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
|
||||
{basics.phone}
|
||||
</a>
|
||||
</div>
|
||||
<div className="h-1 w-1 rounded-full bg-background last:hidden" />
|
||||
</>
|
||||
)}
|
||||
{basics.email && (
|
||||
<>
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<i className="ph ph-bold ph-at" />
|
||||
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
|
||||
{basics.email}
|
||||
</a>
|
||||
</div>
|
||||
<div className="h-1 w-1 rounded-full bg-background last:hidden" />
|
||||
</>
|
||||
)}
|
||||
{isUrl(basics.url.href) && (
|
||||
<>
|
||||
<Link url={basics.url} />
|
||||
<div className="h-1 w-1 rounded-full bg-background last:hidden" />
|
||||
</>
|
||||
)}
|
||||
{basics.customFields.map((item) => (
|
||||
<>
|
||||
<div key={item.id} className="flex items-center gap-x-1.5">
|
||||
<i className={cn(`ph ph-bold ph-${item.icon}`)} />
|
||||
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
|
||||
</div>
|
||||
<div className="h-1 w-1 rounded-full bg-background last:hidden" />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Summary = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.summary);
|
||||
|
||||
if (!section.visible || isEmptyString(section.content)) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className="mb-2 text-base font-bold uppercase">{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
type RatingProps = { level: number };
|
||||
|
||||
const Rating = ({ level }: RatingProps) => (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<i
|
||||
key={index}
|
||||
className={cn(
|
||||
"ph ph-diamond text-primary",
|
||||
level > index && "ph-fill",
|
||||
level <= index && "ph-bold",
|
||||
)}
|
||||
/>
|
||||
// <div
|
||||
// key={index}
|
||||
// className={cn("h-2 w-4 border border-primary", level > index && "bg-primary")}
|
||||
// />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
type LinkProps = {
|
||||
url: URL;
|
||||
icon?: React.ReactNode;
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{icon ?? <i className="ph ph-bold ph-link text-primary group-[.summary]:text-background" />}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SectionProps<T> = {
|
||||
section: SectionWithItem<T> | CustomSectionGroup;
|
||||
children?: (item: T) => React.ReactNode;
|
||||
className?: string;
|
||||
urlKey?: keyof T;
|
||||
levelKey?: keyof T;
|
||||
summaryKey?: keyof T;
|
||||
keywordsKey?: keyof T;
|
||||
};
|
||||
|
||||
const Section = <T,>({
|
||||
section,
|
||||
children,
|
||||
className,
|
||||
urlKey,
|
||||
levelKey,
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
<h4 className="mb-2 border-b border-primary text-base font-bold uppercase">{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid gap-3"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items
|
||||
.filter((item) => item.visible)
|
||||
.map((item) => {
|
||||
const url = (urlKey && get(item, urlKey)) as URL | undefined;
|
||||
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
|
||||
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
|
||||
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
|
||||
|
||||
return (
|
||||
<div key={item.id} className={cn("space-y-2", className)}>
|
||||
<div className="leading-snug">
|
||||
{children?.(item as T)}
|
||||
{url && <Link url={url} />}
|
||||
</div>
|
||||
|
||||
{summary && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
)}
|
||||
|
||||
{level && level > 0 && <Rating level={level} />}
|
||||
|
||||
{keywords && keywords.length > 0 && (
|
||||
<p className="text-sm">{keywords.join(", ")}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Profiles = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
||||
|
||||
return (
|
||||
<Section<Profile> section={section}>
|
||||
{(item) => (
|
||||
<div className="leading-snug">
|
||||
{isUrl(item.url.href) ? (
|
||||
<Link
|
||||
url={item.url}
|
||||
label={item.username}
|
||||
icon={
|
||||
<img
|
||||
width={fontSize}
|
||||
height={fontSize}
|
||||
alt={item.network}
|
||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<p>{item.username}</p>
|
||||
)}
|
||||
<p className="text-sm">{item.network}</p>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Experience = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.experience);
|
||||
|
||||
return (
|
||||
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.company}</div>
|
||||
<div>{item.position}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Education = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.education);
|
||||
|
||||
return (
|
||||
<Section<Education> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.institution}</div>
|
||||
<div>{item.area}</div>
|
||||
<div>{item.score}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.studyType}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Awards = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.awards);
|
||||
|
||||
return (
|
||||
<Section<Award> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.title}</div>
|
||||
<div>{item.awarder}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Certifications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.certifications);
|
||||
|
||||
return (
|
||||
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.issuer}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Skills = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.skills);
|
||||
|
||||
return (
|
||||
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Interests = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.interests);
|
||||
|
||||
return (
|
||||
<Section<Interest> section={section} className="space-y-0" keywordsKey="keywords">
|
||||
{(item) => <div className="font-bold">{item.name}</div>}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Publications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.publications);
|
||||
|
||||
return (
|
||||
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.publisher}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Volunteer = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.volunteer);
|
||||
|
||||
return (
|
||||
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.organization}</div>
|
||||
<div>{item.position}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Languages = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.languages);
|
||||
|
||||
return (
|
||||
<Section<Language> section={section} levelKey="fluencyLevel">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.fluency}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Projects = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.projects);
|
||||
|
||||
return (
|
||||
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const References = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.references);
|
||||
|
||||
return (
|
||||
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Custom = ({ id }: { id: string }) => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
|
||||
|
||||
return (
|
||||
<Section<CustomSection>
|
||||
section={section}
|
||||
urlKey="url"
|
||||
summaryKey="summary"
|
||||
keywordsKey="keywords"
|
||||
>
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between group-[.sidebar]:flex-col group-[.sidebar]:items-start">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const mapSectionToComponent = (section: SectionKey) => {
|
||||
switch (section) {
|
||||
case "profiles":
|
||||
return <Profiles />;
|
||||
case "summary":
|
||||
return <Summary />;
|
||||
case "experience":
|
||||
return <Experience />;
|
||||
case "education":
|
||||
return <Education />;
|
||||
case "awards":
|
||||
return <Awards />;
|
||||
case "certifications":
|
||||
return <Certifications />;
|
||||
case "skills":
|
||||
return <Skills />;
|
||||
case "interests":
|
||||
return <Interests />;
|
||||
case "publications":
|
||||
return <Publications />;
|
||||
case "volunteer":
|
||||
return <Volunteer />;
|
||||
case "languages":
|
||||
return <Languages />;
|
||||
case "projects":
|
||||
return <Projects />;
|
||||
case "references":
|
||||
return <References />;
|
||||
default:
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const Pikachu = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
const [main, sidebar] = columns;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-3 space-x-6">
|
||||
<div className="sidebar group col-span-1 space-y-4">
|
||||
{isFirstPage && <Picture className="w-full !max-w-none" />}
|
||||
|
||||
{sidebar.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="main group col-span-2 space-y-4">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
{main.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,13 +1,30 @@
|
||||
import { SectionKey } from "@reactive-resume/schema";
|
||||
import {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSection,
|
||||
CustomSectionGroup,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
Language,
|
||||
Profile,
|
||||
Project,
|
||||
Publication,
|
||||
Reference,
|
||||
SectionKey,
|
||||
SectionWithItem,
|
||||
Skill,
|
||||
URL,
|
||||
Volunteer,
|
||||
} from "@reactive-resume/schema";
|
||||
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
|
||||
import get from "lodash.get";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { Picture } from "../components/picture";
|
||||
import { useArtboardStore } from "../store/artboard";
|
||||
import { TemplateProps } from "../types/template";
|
||||
|
||||
const fieldDisplay = cn("flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0");
|
||||
|
||||
const Header = () => {
|
||||
const basics = useArtboardStore((state) => state.resume.basics);
|
||||
|
||||
@ -16,43 +33,39 @@ const Header = () => {
|
||||
<Picture />
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-2xl font-bold leading-tight">{basics.name}</div>
|
||||
<div className="text-2xl font-bold">{basics.name}</div>
|
||||
<div className="text-base">{basics.headline}</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm">
|
||||
{basics.location && (
|
||||
<div className={cn(fieldDisplay)}>
|
||||
<i className="ph ph-map-pin" />
|
||||
<div className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0">
|
||||
<i className="ph ph-bold ph-map-pin text-primary" />
|
||||
<div>{basics.location}</div>
|
||||
</div>
|
||||
)}
|
||||
{basics.phone && (
|
||||
<div className={cn(fieldDisplay)}>
|
||||
<i className="ph ph-phone" />
|
||||
<div className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0">
|
||||
<i className="ph ph-bold ph-phone text-primary" />
|
||||
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
|
||||
{basics.phone}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{basics.email && (
|
||||
<div className={cn(fieldDisplay)}>
|
||||
<i className="ph ph-envelope" />
|
||||
<div className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0">
|
||||
<i className="ph ph-bold ph-at text-primary" />
|
||||
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
|
||||
{basics.email}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{isUrl(basics.url.href) && (
|
||||
<div className={cn(fieldDisplay)}>
|
||||
<i className="ph ph-globe" />
|
||||
<a href={basics.url.href} target="_blank" rel="noreferrer">
|
||||
{basics.url.label || basics.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<Link url={basics.url} />
|
||||
{basics.customFields.map((item) => (
|
||||
<div key={item.id} className={cn(fieldDisplay)}>
|
||||
<i className={`ph ph-${item.icon}`} />
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0"
|
||||
>
|
||||
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
|
||||
<span>{[item.name, item.value].filter(Boolean).join(": ")}</span>
|
||||
</div>
|
||||
))}
|
||||
@ -62,580 +75,384 @@ const Header = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const sectionHeading = cn("mb-1.5 mt-3 border-b pb-0.5 text-sm font-bold uppercase");
|
||||
const Summary = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.summary);
|
||||
|
||||
const Profiles = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
if (!section.visible || isEmptyString(section.content)) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold uppercase">{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="mt-2 grid items-center"
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
type RatingProps = { level: number };
|
||||
|
||||
const Rating = ({ level }: RatingProps) => (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn("h-2 w-2 rounded-full border border-primary", level > index && "bg-primary")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
type LinkProps = {
|
||||
url: URL;
|
||||
icon?: React.ReactNode;
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Link = ({ url, icon, label, className }: LinkProps) => {
|
||||
if (!isUrl(url.href)) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
|
||||
<a
|
||||
href={url.href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener nofollow"
|
||||
className={cn("inline-block", className)}
|
||||
>
|
||||
{label || url.label || url.href}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SectionProps<T> = {
|
||||
section: SectionWithItem<T> | CustomSectionGroup;
|
||||
children?: (item: T) => React.ReactNode;
|
||||
className?: string;
|
||||
urlKey?: keyof T;
|
||||
levelKey?: keyof T;
|
||||
summaryKey?: keyof T;
|
||||
keywordsKey?: keyof T;
|
||||
};
|
||||
|
||||
const Section = <T,>({
|
||||
section,
|
||||
children,
|
||||
className,
|
||||
urlKey,
|
||||
levelKey,
|
||||
summaryKey,
|
||||
keywordsKey,
|
||||
}: SectionProps<T>) => {
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold uppercase">{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid gap-3"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-x-3">
|
||||
<img
|
||||
width="16"
|
||||
height="16"
|
||||
alt={item.network}
|
||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
||||
/>
|
||||
{section.items
|
||||
.filter((item) => item.visible)
|
||||
.map((item) => {
|
||||
const url = (urlKey && get(item, urlKey)) as URL | undefined;
|
||||
const level = (levelKey && get(item, levelKey, 0)) as number | undefined;
|
||||
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
|
||||
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
|
||||
|
||||
<div className="leading-tight">
|
||||
{isUrl(item.url.href) ? (
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer" className="font-medium">
|
||||
{item.url.label || item.username}
|
||||
</a>
|
||||
) : (
|
||||
<span className="font-medium">{item.username}</span>
|
||||
)}
|
||||
return (
|
||||
<div key={item.id} className={cn("space-y-2", className)}>
|
||||
<div className="leading-snug">
|
||||
{children?.(item as T)}
|
||||
{url && <Link url={url} />}
|
||||
</div>
|
||||
|
||||
<p className="text-sm">{item.network}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{summary && !isEmptyString(summary) && (
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
|
||||
)}
|
||||
|
||||
{level && level > 0 && <Rating level={level} />}
|
||||
|
||||
{keywords && keywords.length > 0 && (
|
||||
<p className="text-sm">{keywords.join(", ")}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const Summary = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.summary);
|
||||
|
||||
if (!section.visible || !section.content) return null;
|
||||
const Profiles = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
||||
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
{!isEmptyString(section.content) && (
|
||||
<main>
|
||||
<div
|
||||
className="wysiwyg"
|
||||
style={{ columns: section.columns }}
|
||||
dangerouslySetInnerHTML={{ __html: section.content }}
|
||||
/>
|
||||
</main>
|
||||
<Section<Profile> section={section}>
|
||||
{(item) => (
|
||||
<div className="leading-snug">
|
||||
{isUrl(item.url.href) ? (
|
||||
<Link
|
||||
url={item.url}
|
||||
label={item.username}
|
||||
icon={
|
||||
<img
|
||||
width={fontSize}
|
||||
height={fontSize}
|
||||
alt={item.network}
|
||||
src={`https://cdn.simpleicons.org/${item.icon}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<p>{item.username}</p>
|
||||
)}
|
||||
<p className="text-sm">{item.network}</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Experience = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.experience);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id}>
|
||||
<header className="mb-2 flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.company}</div>
|
||||
<div>{item.position}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
<Section<Experience> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.company}</div>
|
||||
<div>{item.position}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Education = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.education);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id}>
|
||||
<header className="mb-2 flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.institution}</div>
|
||||
<div>{item.area}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.studyType}</div>
|
||||
<div>{item.score}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
<Section<Education> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.institution}</div>
|
||||
<div>{item.area}</div>
|
||||
<div>{item.score}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.studyType}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Awards = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.awards);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.title}</div>
|
||||
<div>{item.awarder}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
<Section<Award> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.title}</div>
|
||||
<div>{item.awarder}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Certifications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.certifications);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.issuer}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
<Section<Certification> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.issuer}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Skills = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.skills);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{item.keywords.length > 0 && (
|
||||
<footer>
|
||||
<p className="text-sm">{item.keywords.join(", ")}</p>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div className="leading-tight">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Interests = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.interests);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id}>
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{item.keywords.length > 0 && (
|
||||
<footer>
|
||||
<p className="text-sm">{item.keywords.join(", ")}</p>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<Section<Interest> section={section} keywordsKey="keywords" className="space-y-0.5">
|
||||
{(item) => <div className="font-bold">{item.name}</div>}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Publications = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.publications);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.publisher}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
<Section<Publication> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.publisher}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Volunteer = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.volunteer);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id}>
|
||||
<header className="mb-2 flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.organization}</div>
|
||||
<div>{item.position}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
<Section<Volunteer> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.organization}</div>
|
||||
<div>{item.position}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Languages = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.languages);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.fluency}</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<Section<Language> section={section} levelKey="fluencyLevel">
|
||||
{(item) => (
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.fluency}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Projects = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.projects);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
|
||||
{item.keywords.length > 0 && (
|
||||
<footer>
|
||||
<p className="text-sm">{item.keywords.join(", ")}</p>
|
||||
</footer>
|
||||
)}
|
||||
<Section<Project> section={section} urlKey="url" summaryKey="summary" keywordsKey="keywords">
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const References = () => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.references);
|
||||
|
||||
if (!section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
|
||||
{(item) => (
|
||||
<div>
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Custom = ({ id }: { id: string }) => {
|
||||
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
|
||||
|
||||
if (!section || !section.visible || !section.items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id}>
|
||||
<h4 className={cn(sectionHeading)}>{section.name}</h4>
|
||||
|
||||
<div
|
||||
className="grid items-start gap-x-4 gap-y-2"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{section.items.map((item) => (
|
||||
<div key={item.id} className="space-y-2">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
{isUrl(item.url.href) && (
|
||||
<div>
|
||||
<a href={item.url.href} target="_blank" rel="noreferrer">
|
||||
{item.url.label || item.url.href}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!isEmptyString(item.summary) && (
|
||||
<main>
|
||||
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: item.summary }} />
|
||||
</main>
|
||||
)}
|
||||
|
||||
{item.keywords.length > 0 && (
|
||||
<footer>
|
||||
<p className="text-sm">{item.keywords.join(", ")}</p>
|
||||
</footer>
|
||||
)}
|
||||
<Section<CustomSection>
|
||||
section={section}
|
||||
urlKey="url"
|
||||
summaryKey="summary"
|
||||
keywordsKey="keywords"
|
||||
>
|
||||
{(item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-left">
|
||||
<div className="font-bold">{item.name}</div>
|
||||
<div>{item.description}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="font-bold">{item.date}</div>
|
||||
<div>{item.location}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@ -670,7 +487,7 @@ const mapSectionToComponent = (section: SectionKey) => {
|
||||
default:
|
||||
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
|
||||
|
||||
return <p>{section}</p>;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@ -681,7 +498,7 @@ export const Rhyhorn = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
<div className="space-y-4">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-4">
|
||||
{main.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
|
||||
@ -8,6 +8,21 @@ module.exports = {
|
||||
join(__dirname, "{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}"),
|
||||
...createGlobPatternsForDependencies(__dirname),
|
||||
],
|
||||
theme: {},
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
text: "var(--color-text)",
|
||||
primary: "var(--color-primary)",
|
||||
background: "var(--color-background)",
|
||||
},
|
||||
lineHeight: {
|
||||
tight: "calc(var(--line-height) - 0.5)",
|
||||
snug: "calc(var(--line-height) - 0.3)",
|
||||
normal: "var(--line-height)",
|
||||
relaxed: "calc(var(--line-height) + 0.3)",
|
||||
loose: "calc(var(--line-height) + 0.5)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/typography")],
|
||||
};
|
||||
|
||||
@ -2,20 +2,24 @@
|
||||
|
||||
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import { defineConfig, searchForWorkspaceRoot } from "vite";
|
||||
import { defineConfig, searchForWorkspaceRoot, splitVendorChunkPlugin } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
base: "/artboard/",
|
||||
|
||||
cacheDir: "../../node_modules/.vite/artboard",
|
||||
|
||||
build: {
|
||||
sourcemap: true,
|
||||
},
|
||||
|
||||
server: {
|
||||
host: true,
|
||||
port: 6173,
|
||||
port: +(process.env.__DEV__ARTBOARD_PORT ?? 6173),
|
||||
fs: { allow: [searchForWorkspaceRoot(process.cwd())] },
|
||||
},
|
||||
|
||||
plugins: [react(), nxViteTsPaths()],
|
||||
plugins: [react(), nxViteTsPaths(), splitVendorChunkPlugin()],
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@ -39,5 +39,8 @@
|
||||
|
||||
<!-- Scripts -->
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
||||
<!-- Phosphor Icons -->
|
||||
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 344 KiB |
BIN
apps/client/public/templates/sample.jpg
Normal file
BIN
apps/client/public/templates/sample.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@ -2,7 +2,7 @@ import { Plus, PlusCircle } from "@phosphor-icons/react";
|
||||
import {
|
||||
Award,
|
||||
Certification,
|
||||
CustomSectionItem,
|
||||
CustomSection,
|
||||
Education,
|
||||
Experience,
|
||||
Interest,
|
||||
@ -172,7 +172,7 @@ export const LeftSidebar = () => {
|
||||
<Fragment key={section.id}>
|
||||
<Separator />
|
||||
|
||||
<SectionBase<CustomSectionItem>
|
||||
<SectionBase<CustomSection>
|
||||
id={`custom.${section.id}`}
|
||||
title={(item) => item.name}
|
||||
description={(item) => item.description}
|
||||
|
||||
@ -19,7 +19,7 @@ export const TemplateSection = () => {
|
||||
</header>
|
||||
|
||||
<main className="grid grid-cols-2 gap-4">
|
||||
{templatesList.map(({ id, name, image }) => (
|
||||
{templatesList.map(({ id, name }) => (
|
||||
<HoverCard key={id} openDelay={0} closeDelay={0}>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button
|
||||
@ -35,7 +35,12 @@ export const TemplateSection = () => {
|
||||
</HoverCardTrigger>
|
||||
|
||||
<HoverCardContent className="max-w-xs overflow-hidden border-none bg-white p-0">
|
||||
<img alt={name} src={image} loading="lazy" className="aspect-[1/1.4142]" />
|
||||
<img
|
||||
alt={name}
|
||||
loading="lazy"
|
||||
src="/templates/sample.jpg"
|
||||
className="aspect-[1/1.4142]"
|
||||
/>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
))}
|
||||
|
||||
@ -75,6 +75,8 @@ export const TypographySection = () => {
|
||||
disabled={typography.font.family === font}
|
||||
onClick={() => {
|
||||
setValue("metadata.typography.font.family", font);
|
||||
setValue("metadata.typography.font.subset", "latin");
|
||||
setValue("metadata.typography.font.variants", ["regular"]);
|
||||
}}
|
||||
className={cn(
|
||||
"flex h-12 items-center justify-center overflow-hidden rounded border text-center text-sm ring-primary transition-colors hover:bg-secondary-accent focus:outline-none focus:ring-1 disabled:opacity-100",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { ResumeDto } from "@reactive-resume/dto";
|
||||
import { CustomSection, defaultSection, SectionKey } from "@reactive-resume/schema";
|
||||
import { CustomSectionGroup, defaultSection, SectionKey } from "@reactive-resume/schema";
|
||||
import { removeItemInLayout } from "@reactive-resume/utils";
|
||||
import _set from "lodash.set";
|
||||
import { temporal, TemporalState } from "zundo";
|
||||
@ -38,7 +38,7 @@ export const useResumeStore = create<ResumeStore>()(
|
||||
});
|
||||
},
|
||||
addSection: () => {
|
||||
const section: CustomSection = {
|
||||
const section: CustomSectionGroup = {
|
||||
...defaultSection,
|
||||
id: createId(),
|
||||
name: "Custom Section",
|
||||
|
||||
Reference in New Issue
Block a user