mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 04:22:27 +10:00
- Modified multiple template files to change the sidebar length conditionals from `!== 0` to `> 0` for improved readability and consistency across components. - Added Code of Conduct
576 lines
16 KiB
TypeScript
576 lines
16 KiB
TypeScript
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 { BrandIcon } from "../components/brand-icon";
|
|
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")} />
|
|
{isUrl(item.value) ? (
|
|
<a href={item.value} target="_blank" rel="noreferrer noopener nofollow">
|
|
{item.name || item.value}
|
|
</a>
|
|
) : (
|
|
<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 text-primary group-[.main]:block">
|
|
<h4>{section.name}</h4>
|
|
</div>
|
|
|
|
<div className="mb-2 hidden items-center gap-x-2 text-center font-bold text-primary group-[.sidebar]:flex">
|
|
<div className="size-1.5 rounded-full border border-primary" />
|
|
<h4>{section.name}</h4>
|
|
<div className="size-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 size-[8px] rounded-full bg-primary group-[.main]:block" />
|
|
|
|
<div
|
|
dangerouslySetInnerHTML={{ __html: section.content }}
|
|
className="wysiwyg"
|
|
style={{ columns: section.columns }}
|
|
/>
|
|
</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;
|
|
iconOnRight?: boolean;
|
|
label?: string;
|
|
className?: string;
|
|
};
|
|
|
|
const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
|
|
if (!isUrl(url.href)) return null;
|
|
|
|
return (
|
|
<div className="flex items-center gap-x-1.5">
|
|
{!iconOnRight && (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>
|
|
{iconOnRight && (icon ?? <i className="ph ph-bold ph-link text-primary" />)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type LinkedEntityProps = {
|
|
name: string;
|
|
url: URL;
|
|
separateLinks: boolean;
|
|
className?: string;
|
|
};
|
|
|
|
const LinkedEntity = ({ name, url, separateLinks, className }: LinkedEntityProps) => {
|
|
return !separateLinks && isUrl(url.href) ? (
|
|
<Link
|
|
url={url}
|
|
label={name}
|
|
icon={<i className="ph ph-bold ph-globe text-primary" />}
|
|
iconOnRight={true}
|
|
className={className}
|
|
/>
|
|
) : (
|
|
<div className={className}>{name}</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 === 0) return null;
|
|
|
|
return (
|
|
<section id={section.id} className="grid">
|
|
<div className="mb-2 hidden font-bold 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 text-primary group-[.sidebar]:flex">
|
|
<div className="size-1.5 rounded-full border border-primary" />
|
|
<h4>{section.name}</h4>
|
|
<div className="size-1.5 rounded-full border border-primary" />
|
|
</div>
|
|
|
|
<div
|
|
className="grid gap-x-6 gap-y-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>{children?.(item as T)}</div>
|
|
|
|
{summary !== undefined && !isEmptyString(summary) && (
|
|
<div dangerouslySetInnerHTML={{ __html: summary }} className="wysiwyg" />
|
|
)}
|
|
|
|
{level !== undefined && level > 0 && <Rating level={level} />}
|
|
|
|
{keywords !== undefined && keywords.length > 0 && (
|
|
<p className="text-sm">{keywords.join(", ")}</p>
|
|
)}
|
|
|
|
{url !== undefined && section.separateLinks && <Link url={url} />}
|
|
|
|
<div className="absolute left-[-4.5px] top-px hidden size-[8px] rounded-full bg-primary group-[.main]:block" />
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const Profiles = () => {
|
|
const section = useArtboardStore((state) => state.resume.sections.profiles);
|
|
|
|
return (
|
|
<Section<Profile> section={section}>
|
|
{(item) => (
|
|
<div>
|
|
{isUrl(item.url.href) ? (
|
|
<Link url={item.url} label={item.username} icon={<BrandIcon slug={item.icon} />} />
|
|
) : (
|
|
<p>{item.username}</p>
|
|
)}
|
|
{!item.icon && <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>
|
|
<LinkedEntity
|
|
name={item.company}
|
|
url={item.url}
|
|
separateLinks={section.separateLinks}
|
|
className="font-bold"
|
|
/>
|
|
<div>{item.position}</div>
|
|
<div>{item.location}</div>
|
|
<div className="font-bold">{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>
|
|
<LinkedEntity
|
|
name={item.institution}
|
|
url={item.url}
|
|
separateLinks={section.separateLinks}
|
|
className="font-bold"
|
|
/>
|
|
<div>{item.area}</div>
|
|
<div>{item.score}</div>
|
|
<div>{item.studyType}</div>
|
|
<div className="font-bold">{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>
|
|
<LinkedEntity name={item.awarder} url={item.url} separateLinks={section.separateLinks} />
|
|
<div className="font-bold">{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>
|
|
<LinkedEntity name={item.issuer} url={item.url} separateLinks={section.separateLinks} />
|
|
<div className="font-bold">{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>
|
|
<LinkedEntity
|
|
name={item.name}
|
|
url={item.url}
|
|
separateLinks={section.separateLinks}
|
|
className="font-bold"
|
|
/>
|
|
<div>{item.publisher}</div>
|
|
<div className="font-bold">{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>
|
|
<LinkedEntity
|
|
name={item.organization}
|
|
url={item.url}
|
|
separateLinks={section.separateLinks}
|
|
className="font-bold"
|
|
/>
|
|
<div>{item.position}</div>
|
|
<div>{item.location}</div>
|
|
<div className="font-bold">{item.date}</div>
|
|
</div>
|
|
)}
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
const Languages = () => {
|
|
const section = useArtboardStore((state) => state.resume.sections.languages);
|
|
|
|
return (
|
|
<Section<Language> section={section} levelKey="level">
|
|
{(item) => (
|
|
<div>
|
|
<div className="font-bold">{item.name}</div>
|
|
<div>{item.description}</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>
|
|
<LinkedEntity
|
|
name={item.name}
|
|
url={item.url}
|
|
separateLinks={section.separateLinks}
|
|
className="font-bold"
|
|
/>
|
|
<div>{item.description}</div>
|
|
|
|
<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>
|
|
<LinkedEntity
|
|
name={item.name}
|
|
url={item.url}
|
|
separateLinks={section.separateLinks}
|
|
className="font-bold"
|
|
/>
|
|
<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>
|
|
<LinkedEntity
|
|
name={item.name}
|
|
url={item.url}
|
|
separateLinks={section.separateLinks}
|
|
className="font-bold"
|
|
/>
|
|
<div>{item.description}</div>
|
|
|
|
<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 Azurill = ({ columns, isFirstPage = false }: TemplateProps) => {
|
|
const [main, sidebar] = columns;
|
|
|
|
return (
|
|
<div className="p-custom space-y-3">
|
|
{isFirstPage && <Header />}
|
|
|
|
<div className="grid grid-cols-3 gap-x-4">
|
|
<div className="sidebar group space-y-4">
|
|
{sidebar.map((section) => (
|
|
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
|
))}
|
|
</div>
|
|
|
|
<div
|
|
className={cn("main group space-y-4", sidebar.length > 0 ? "col-span-2" : "col-span-3")}
|
|
>
|
|
{main.map((section) => (
|
|
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|