mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 01:15:26 +10:00
- fixes #2562, add better error messages for duplicate resume slugs
- improvements made to ditgar template - general improvements to all templates with backgrounds - update dependencies and translations - improved print function that handles single page and multi page resumes
This commit is contained in:
@@ -77,7 +77,7 @@ export function CommandPalette() {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogHeader className="sr-only print:hidden">
|
||||
<DialogTitle>
|
||||
<Trans>Builder Command Palette</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
@@ -106,7 +106,7 @@ export const ResumePreview = ({ showPageNumbers, pageClassName, className, ...pr
|
||||
const pageNumber = pageIndex + 1;
|
||||
|
||||
return (
|
||||
<div key={pageIndex} className="relative">
|
||||
<div key={pageIndex} data-page-index={pageIndex} className="relative">
|
||||
{showPageNumbers && totalNumberOfPages > 1 && (
|
||||
<div className="absolute -top-6 left-0">
|
||||
<span className="font-medium text-foreground text-xs">
|
||||
@@ -117,14 +117,7 @@ export const ResumePreview = ({ showPageNumbers, pageClassName, className, ...pr
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
`page page-${pageIndex}`,
|
||||
pageIndex > 0 && "print:break-before-page",
|
||||
styles.page,
|
||||
pageClassName,
|
||||
)}
|
||||
>
|
||||
<div className={cn(`page page-${pageIndex}`, styles.page, pageClassName)}>
|
||||
<TemplateComponent pageIndex={pageIndex} pageLayout={pageLayout} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { match } from "ts-pattern";
|
||||
import type { CustomSectionItem, SectionType } from "@/schema/resume/data";
|
||||
import type { CustomSectionItem, SectionItem, SectionType } from "@/schema/resume/data";
|
||||
import { cn } from "@/utils/style";
|
||||
import { useResumeStore } from "../store/resume";
|
||||
import { AwardsItem } from "./items/awards-item";
|
||||
@@ -25,38 +25,22 @@ type SectionComponentProps = {
|
||||
// Helper to render item component based on type
|
||||
function renderItemByType(type: SectionType, item: CustomSectionItem, itemClassName?: string) {
|
||||
return match(type)
|
||||
.with("profiles", () => (
|
||||
<ProfilesItem {...(item as Parameters<typeof ProfilesItem>[0])} className={itemClassName} />
|
||||
))
|
||||
.with("experience", () => (
|
||||
<ExperienceItem {...(item as Parameters<typeof ExperienceItem>[0])} className={itemClassName} />
|
||||
))
|
||||
.with("education", () => (
|
||||
<EducationItem {...(item as Parameters<typeof EducationItem>[0])} className={itemClassName} />
|
||||
))
|
||||
.with("projects", () => (
|
||||
<ProjectsItem {...(item as Parameters<typeof ProjectsItem>[0])} className={itemClassName} />
|
||||
))
|
||||
.with("skills", () => <SkillsItem {...(item as Parameters<typeof SkillsItem>[0])} className={itemClassName} />)
|
||||
.with("languages", () => (
|
||||
<LanguagesItem {...(item as Parameters<typeof LanguagesItem>[0])} className={itemClassName} />
|
||||
))
|
||||
.with("interests", () => (
|
||||
<InterestsItem {...(item as Parameters<typeof InterestsItem>[0])} className={itemClassName} />
|
||||
))
|
||||
.with("awards", () => <AwardsItem {...(item as Parameters<typeof AwardsItem>[0])} className={itemClassName} />)
|
||||
.with("profiles", () => <ProfilesItem {...(item as SectionItem<"profiles">)} className={itemClassName} />)
|
||||
.with("experience", () => <ExperienceItem {...(item as SectionItem<"experience">)} className={itemClassName} />)
|
||||
.with("education", () => <EducationItem {...(item as SectionItem<"education">)} className={itemClassName} />)
|
||||
.with("projects", () => <ProjectsItem {...(item as SectionItem<"projects">)} className={itemClassName} />)
|
||||
.with("skills", () => <SkillsItem {...(item as SectionItem<"skills">)} className={itemClassName} />)
|
||||
.with("languages", () => <LanguagesItem {...(item as SectionItem<"languages">)} className={itemClassName} />)
|
||||
.with("interests", () => <InterestsItem {...(item as SectionItem<"interests">)} className={itemClassName} />)
|
||||
.with("awards", () => <AwardsItem {...(item as SectionItem<"awards">)} className={itemClassName} />)
|
||||
.with("certifications", () => (
|
||||
<CertificationsItem {...(item as Parameters<typeof CertificationsItem>[0])} className={itemClassName} />
|
||||
<CertificationsItem {...(item as SectionItem<"certifications">)} className={itemClassName} />
|
||||
))
|
||||
.with("publications", () => (
|
||||
<PublicationsItem {...(item as Parameters<typeof PublicationsItem>[0])} className={itemClassName} />
|
||||
))
|
||||
.with("volunteer", () => (
|
||||
<VolunteerItem {...(item as Parameters<typeof VolunteerItem>[0])} className={itemClassName} />
|
||||
))
|
||||
.with("references", () => (
|
||||
<ReferencesItem {...(item as Parameters<typeof ReferencesItem>[0])} className={itemClassName} />
|
||||
<PublicationsItem {...(item as SectionItem<"publications">)} className={itemClassName} />
|
||||
))
|
||||
.with("volunteer", () => <VolunteerItem {...(item as SectionItem<"volunteer">)} className={itemClassName} />)
|
||||
.with("references", () => <ReferencesItem {...(item as SectionItem<"references">)} className={itemClassName} />)
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
@@ -186,7 +170,10 @@ export function getSectionComponent(
|
||||
style={{ gridTemplateColumns: `repeat(${customSection.columns}, 1fr)` }}
|
||||
>
|
||||
{visibleItems.map((item) => (
|
||||
<div key={item.id} className={cn(`section-item section-item-${customSection.type}`)}>
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(`section-item section-item-${customSection.type} print:break-inside-avoid`)}
|
||||
>
|
||||
{renderItemByType(customSection.type, item, itemClassName)}
|
||||
</div>
|
||||
))}
|
||||
@@ -194,6 +181,7 @@ export function getSectionComponent(
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
return CustomSectionComponent;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,21 +10,27 @@ type AwardsItemProps = SectionItem<"awards"> & {
|
||||
export function AwardsItem({ className, ...item }: AwardsItemProps) {
|
||||
return (
|
||||
<div className={cn("awards-item", className)}>
|
||||
{/* Header */}
|
||||
<div className="section-item-header awards-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-title awards-item-title">
|
||||
<strong>{item.title}</strong>
|
||||
</p>
|
||||
<p className="section-item-metadata awards-item-date text-right">{item.date}</p>
|
||||
<strong className="section-item-title awards-item-title">{item.title}</strong>
|
||||
<span className="section-item-metadata awards-item-date text-right">{item.date}</span>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-metadata awards-item-awarder">{item.awarder}</p>
|
||||
<span className="section-item-metadata awards-item-awarder">{item.awarder}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="section-item-description awards-item-description">
|
||||
<TiptapContent content={item.description} />
|
||||
</div>
|
||||
<div className="section-item-link awards-item-link">
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website awards-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,21 +10,27 @@ type CertificationsItemProps = SectionItem<"certifications"> & {
|
||||
export function CertificationsItem({ className, ...item }: CertificationsItemProps) {
|
||||
return (
|
||||
<div className={cn("certifications-item", className)}>
|
||||
{/* Header */}
|
||||
<div className="section-item-header certifications-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-title certifications-item-title">
|
||||
<strong>{item.title}</strong>
|
||||
</p>
|
||||
<p className="section-item-metadata certifications-item-date text-right">{item.date}</p>
|
||||
<strong className="section-item-title certifications-item-title">{item.title}</strong>
|
||||
<span className="section-item-metadata certifications-item-date text-right">{item.date}</span>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-metadata certifications-item-issuer">{item.issuer}</p>
|
||||
<span className="section-item-metadata certifications-item-issuer">{item.issuer}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="section-item-description certifications-item-description">
|
||||
<TiptapContent content={item.description} />
|
||||
</div>
|
||||
<div className="section-item-link certifications-item-link">
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website certifications-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,26 +10,32 @@ type EducationItemProps = SectionItem<"education"> & {
|
||||
export function EducationItem({ className, ...item }: EducationItemProps) {
|
||||
return (
|
||||
<div className={cn("education-item", className)}>
|
||||
{/* Header */}
|
||||
<div className="section-item-header education-item-header mb-2">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-title education-item-title">
|
||||
<strong>{item.school}</strong>
|
||||
</p>
|
||||
<p className="section-item-metadata education-item-degree-grade text-right">
|
||||
<strong className="section-item-title education-item-title">{item.school}</strong>
|
||||
<span className="section-item-metadata education-item-degree-grade text-right">
|
||||
{[item.degree, item.grade].filter(Boolean).join(" • ")}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-metadata education-item-area">{item.area}</p>
|
||||
<p className="section-item-metadata education-item-location-period text-right">
|
||||
<span className="section-item-metadata education-item-area">{item.area}</span>
|
||||
<span className="section-item-metadata education-item-location-period text-right">
|
||||
{[item.location, item.period].filter(Boolean).join(" • ")}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="section-item-description education-item-description">
|
||||
<TiptapContent content={item.description} />
|
||||
</div>
|
||||
<div className="section-item-link education-item-link">
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website education-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,22 +10,28 @@ type ExperienceItemProps = SectionItem<"experience"> & {
|
||||
export function ExperienceItem({ className, ...item }: ExperienceItemProps) {
|
||||
return (
|
||||
<div className={cn("experience-item", className)}>
|
||||
{/* Header */}
|
||||
<div className="section-item-header experience-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-title experience-item-title">
|
||||
<strong>{item.company}</strong>
|
||||
</p>
|
||||
<p className="section-item-metadata experience-item-location text-right">{item.location}</p>
|
||||
<strong className="section-item-title experience-item-title">{item.company}</strong>
|
||||
<span className="section-item-metadata experience-item-location text-right">{item.location}</span>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-metadata experience-item-position">{item.position}</p>
|
||||
<p className="section-item-metadata experience-item-period text-right">{item.period}</p>
|
||||
<span className="section-item-metadata experience-item-position">{item.position}</span>
|
||||
<span className="section-item-metadata experience-item-period text-right">{item.period}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="section-item-description experience-item-description">
|
||||
<TiptapContent content={item.description} />
|
||||
</div>
|
||||
<div className="section-item-link experience-item-link">
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website experience-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,14 +9,14 @@ type InterestsItemProps = SectionItem<"interests"> & {
|
||||
export function InterestsItem({ className, ...item }: InterestsItemProps) {
|
||||
return (
|
||||
<div className={cn("interests-item", className)}>
|
||||
{/* Header */}
|
||||
<div className="section-item-header flex items-center gap-x-1.5">
|
||||
<PageIcon icon={item.icon} className="section-item-icon interests-item-icon shrink-0" />
|
||||
<p className="section-item-title interests-item-name">
|
||||
<strong>{item.name}</strong>
|
||||
</p>
|
||||
<PageIcon icon={item.icon} className="section-item-icon interests-item-icon" />
|
||||
<strong className="section-item-title interests-item-name">{item.name}</strong>
|
||||
</div>
|
||||
|
||||
<p className="section-item-keywords interests-item-keywords opacity-80">{item.keywords.join(", ")}</p>
|
||||
{/* Keywords */}
|
||||
<span className="section-item-keywords interests-item-keywords opacity-80">{item.keywords.join(", ")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,13 +9,16 @@ type LanguagesItemProps = SectionItem<"languages"> & {
|
||||
export function LanguagesItem({ className, ...item }: LanguagesItemProps) {
|
||||
return (
|
||||
<div className={cn("languages-item", className)}>
|
||||
<div className="section-item-header">
|
||||
<p className="section-item-title languages-item-name">
|
||||
<strong>{item.language}</strong>
|
||||
</p>
|
||||
<p className="section-item-metadata languages-item-fluency opacity-80">{item.fluency}</p>
|
||||
{/* Header */}
|
||||
<div className="section-item-header flex flex-col">
|
||||
{/* Row 1 */}
|
||||
<strong className="section-item-title languages-item-name">{item.language}</strong>
|
||||
|
||||
{/* Row 2 */}
|
||||
<span className="section-item-metadata languages-item-fluency opacity-80">{item.fluency}</span>
|
||||
</div>
|
||||
|
||||
{/* Level */}
|
||||
<PageLevel level={item.level} className="section-item-level languages-item-level" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,17 +10,17 @@ type ProfilesItemProps = SectionItem<"profiles"> & {
|
||||
export function ProfilesItem({ className, ...item }: ProfilesItemProps) {
|
||||
return (
|
||||
<div className={cn("profiles-item", className)}>
|
||||
{/* Header */}
|
||||
<div className="section-item-header flex items-center gap-x-1.5">
|
||||
<PageIcon icon={item.icon} className="section-item-icon profiles-item-icon shrink-0" />
|
||||
<p className="section-item-title profiles-item-network">
|
||||
<strong>{item.network}</strong>
|
||||
</p>
|
||||
<PageIcon icon={item.icon} className="section-item-icon profiles-item-icon" />
|
||||
<strong className="section-item-title profiles-item-network">{item.network}</strong>
|
||||
</div>
|
||||
|
||||
{/* Website */}
|
||||
<PageLink
|
||||
{...item.website}
|
||||
label={item.website.label || item.username}
|
||||
className="section-item-link profiles-item-link"
|
||||
className="section-item-website profiles-item-website"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,18 +10,22 @@ type ProjectsItemProps = SectionItem<"projects"> & {
|
||||
export function ProjectsItem({ className, ...item }: ProjectsItemProps) {
|
||||
return (
|
||||
<div className={cn("projects-item", className)}>
|
||||
{/* Header */}
|
||||
<div className="section-item-header projects-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-title projects-item-title">
|
||||
<strong>{item.name}</strong>
|
||||
</p>
|
||||
<p className="section-item-metadata projects-item-period text-right">{item.period}</p>
|
||||
<strong className="section-item-title projects-item-title">{item.name}</strong>
|
||||
<span className="section-item-metadata projects-item-period text-right">{item.period}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="section-item-description projects-item-description">
|
||||
<TiptapContent content={item.description} />
|
||||
</div>
|
||||
<div className="section-item-link projects-item-link">
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website projects-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,21 +10,27 @@ type PublicationsItemProps = SectionItem<"publications"> & {
|
||||
export function PublicationsItem({ className, ...item }: PublicationsItemProps) {
|
||||
return (
|
||||
<div className={cn("publications-item", className)}>
|
||||
{/* Header */}
|
||||
<div className="section-item-header publications-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-title publications-item-title">
|
||||
<strong>{item.title}</strong>
|
||||
</p>
|
||||
<p className="section-item-metadata publications-item-date text-right">{item.date}</p>
|
||||
<strong className="section-item-title publications-item-title">{item.title}</strong>
|
||||
<span className="section-item-metadata publications-item-date text-right">{item.date}</span>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-metadata publications-item-publisher">{item.publisher}</p>
|
||||
<span className="section-item-metadata publications-item-publisher">{item.publisher}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="section-item-description publications-item-description">
|
||||
<TiptapContent content={item.description} />
|
||||
</div>
|
||||
<div className="section-item-link publications-item-link">
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website publications-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,20 +10,37 @@ type ReferencesItemProps = SectionItem<"references"> & {
|
||||
export function ReferencesItem({ className, ...item }: ReferencesItemProps) {
|
||||
return (
|
||||
<div className={cn("references-item", className)}>
|
||||
{/* Header */}
|
||||
<div className="section-item-header references-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-title references-item-name">
|
||||
<strong>{item.name}</strong>
|
||||
</p>
|
||||
<strong className="section-item-title references-item-name">{item.name}</strong>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="section-item-metadata references-item-position">{item.position}</span>
|
||||
</div>
|
||||
<p className="section-item-metadata references-item-position">{item.position}</p>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="section-item-description references-item-description">
|
||||
<TiptapContent content={item.description} />
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="section-item-footer references-item-footer flex flex-col">
|
||||
<p className="section-item-metadata references-item-phone">{item.phone}</p>
|
||||
<PageLink {...item.website} label={item.website.label} className="section-item-link references-item-link" />
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="section-item-metadata references-item-phone">{item.phone}</span>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<PageLink
|
||||
{...item.website}
|
||||
label={item.website.label}
|
||||
className="section-item-website references-item-website"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,18 +10,23 @@ type SkillsItemProps = SectionItem<"skills"> & {
|
||||
export function SkillsItem({ className, ...item }: SkillsItemProps) {
|
||||
return (
|
||||
<div className={cn("skills-item", className)}>
|
||||
{/* Header */}
|
||||
<div className="section-item-header flex items-center gap-x-1.5">
|
||||
<PageIcon icon={item.icon} className="section-item-icon skills-item-icon shrink-0" />
|
||||
<p className="section-item-title skills-item-name">
|
||||
<strong>{item.name}</strong>
|
||||
</p>
|
||||
<PageIcon icon={item.icon} className="section-item-icon skills-item-icon" />
|
||||
<strong className="section-item-title skills-item-name">{item.name}</strong>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="section-item-metadata skills-item-proficiency opacity-80">{item.proficiency}</p>
|
||||
<small className="section-item-keywords skills-item-keywords">{item.keywords.join(", ")}</small>
|
||||
{/* Proficiency */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="section-item-metadata skills-item-proficiency">{item.proficiency}</span>
|
||||
</div>
|
||||
|
||||
{/* Keywords */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="section-item-keywords skills-item-keywords opacity-80">{item.keywords.join(", ")}</span>
|
||||
</div>
|
||||
|
||||
{/* Level */}
|
||||
<PageLevel level={item.level} className="section-item-level skills-item-level" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,21 +10,27 @@ type VolunteerItemProps = SectionItem<"volunteer"> & {
|
||||
export function VolunteerItem({ className, ...item }: VolunteerItemProps) {
|
||||
return (
|
||||
<div className={cn("volunteer-item", className)}>
|
||||
{/* Header */}
|
||||
<div className="section-item-header volunteer-item-header">
|
||||
{/* Row 1 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-title volunteer-item-title">
|
||||
<strong>{item.organization}</strong>
|
||||
</p>
|
||||
<p className="section-item-metadata volunteer-item-period text-right">{item.period}</p>
|
||||
<strong className="section-item-title volunteer-item-title">{item.organization}</strong>
|
||||
<span className="section-item-metadata volunteer-item-period text-right">{item.period}</span>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="section-item-metadata volunteer-item-location">{item.location}</p>
|
||||
<span className="section-item-metadata volunteer-item-location">{item.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="section-item-description volunteer-item-description">
|
||||
<TiptapContent content={item.description} />
|
||||
</div>
|
||||
<div className="section-item-link volunteer-item-link">
|
||||
|
||||
{/* Website */}
|
||||
<div className="section-item-website volunteer-item-website">
|
||||
<PageLink {...item.website} label={item.website.label} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ export function PageIcon({ icon, className }: { icon: string; className?: string
|
||||
|
||||
return (
|
||||
<i
|
||||
className={cn("ph", `ph-${icon}`, className)}
|
||||
className={cn("ph shrink-0", `ph-${icon}`, className)}
|
||||
style={{ fontSize: `${iconContext.size}px`, color: iconContext.color }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -9,5 +9,5 @@ type Props = React.ComponentProps<"div"> & {
|
||||
export function PageLevel({ level, className, ...props }: Props) {
|
||||
const { icon, type } = useResumeStore((state) => state.resume.data.metadata.design.level);
|
||||
|
||||
return <LevelDisplay icon={icon} type={type} level={level} className={cn("h-4", className)} {...props} />;
|
||||
return <LevelDisplay icon={icon} type={type} level={level} className={cn("h-6", className)} {...props} />;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export function PageLink({ url, label, className }: Props) {
|
||||
if (!url) return null;
|
||||
|
||||
return (
|
||||
<a href={url} target="_blank" rel="noopener" className={cn("inline-block text-wrap", className)}>
|
||||
<a href={url} target="_blank" rel="noopener" className={cn("inline-block text-wrap break-all", className)}>
|
||||
{label || url}
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -19,14 +19,14 @@ export function PageSection<T extends SectionType>({ type, className, children }
|
||||
|
||||
return (
|
||||
<section className={cn(`page-section page-section-${type}`, className)}>
|
||||
<h6 className="mb-1 text-(--page-primary-color)">{section.title || getSectionTitle(type)}</h6>
|
||||
<h6 className="mb-2 text-(--page-primary-color)">{section.title || getSectionTitle(type)}</h6>
|
||||
|
||||
<div
|
||||
className="section-content grid gap-x-(--page-gap-x) gap-y-(--page-gap-y)"
|
||||
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className={cn(`section-item section-item-${type} *:space-y-1`)}>
|
||||
<div key={item.id} className={cn(`section-item section-item-${type} print:break-inside-avoid`)}>
|
||||
{children(item)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -25,9 +25,8 @@ type ResumeStore = ResumeStoreState & ResumeStoreActions;
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
const _syncResume = async (resume: Resume | null) => {
|
||||
if (!resume) return;
|
||||
await orpc.resume.update.call({ id: resume.id, data: resume.data }, { signal });
|
||||
const _syncResume = (resume: Resume) => {
|
||||
orpc.resume.update.call({ id: resume.id, data: resume.data }, { signal });
|
||||
};
|
||||
|
||||
const syncResume = debounce(_syncResume, 500, { signal });
|
||||
@@ -49,12 +48,13 @@ export const useResumeStore = create<ResumeStore>()(
|
||||
updateResumeData: (fn) => {
|
||||
set((state) => {
|
||||
if (!state.resume) return state;
|
||||
|
||||
if (state.resume.isLocked) {
|
||||
errorToastId = toast.error(t`This resume is locked and cannot be updated.`, { id: errorToastId });
|
||||
return state;
|
||||
}
|
||||
|
||||
fn(state.resume.data as WritableDraft<ResumeData>);
|
||||
fn(state.resume.data);
|
||||
syncResume(current(state.resume));
|
||||
});
|
||||
},
|
||||
|
||||
@@ -8,6 +8,26 @@ import { useResumeStore } from "../store/resume";
|
||||
import type { TemplateProps } from "./types";
|
||||
|
||||
const sectionClassName = cn(
|
||||
// Heading Decoration in Sidebar Layout
|
||||
"group-data-[layout=sidebar]:[&>h6]:px-4",
|
||||
"group-data-[layout=sidebar]:[&>h6]:relative",
|
||||
"group-data-[layout=sidebar]:[&>h6]:inline-flex",
|
||||
"group-data-[layout=sidebar]:[&>h6]:items-center",
|
||||
"group-data-[layout=sidebar]:[&>h6]:before:content-['']",
|
||||
"group-data-[layout=sidebar]:[&>h6]:before:absolute",
|
||||
"group-data-[layout=sidebar]:[&>h6]:before:left-0",
|
||||
"group-data-[layout=sidebar]:[&>h6]:before:rounded-full",
|
||||
"group-data-[layout=sidebar]:[&>h6]:before:size-2",
|
||||
"group-data-[layout=sidebar]:[&>h6]:before:border",
|
||||
"group-data-[layout=sidebar]:[&>h6]:before:border-(--page-primary-color)",
|
||||
"group-data-[layout=sidebar]:[&>h6]:after:content-['']",
|
||||
"group-data-[layout=sidebar]:[&>h6]:after:absolute",
|
||||
"group-data-[layout=sidebar]:[&>h6]:after:right-0",
|
||||
"group-data-[layout=sidebar]:[&>h6]:after:rounded-full",
|
||||
"group-data-[layout=sidebar]:[&>h6]:after:size-2",
|
||||
"group-data-[layout=sidebar]:[&>h6]:after:border",
|
||||
"group-data-[layout=sidebar]:[&>h6]:after:border-(--page-primary-color)",
|
||||
|
||||
// Section in Sidebar Layout
|
||||
"group-data-[layout=sidebar]:[&_.section-item-header>div]:flex-col",
|
||||
"group-data-[layout=sidebar]:[&_.section-item-header>div]:items-start",
|
||||
|
||||
@@ -18,6 +18,10 @@ const sectionClassName = cn(
|
||||
// Icon Colors in Sidebar Layout
|
||||
"group-data-[layout=sidebar]:[&_.section-item_i]:text-(--page-background-color)!",
|
||||
|
||||
// Level Display in Sidebar Layout
|
||||
"group-data-[layout=sidebar]:[&_.section-item-level>div]:bg-(--page-background-color)",
|
||||
"group-data-[layout=sidebar]:[&_.section-item-level>div]:text-(--page-background-color)",
|
||||
|
||||
// Section Item Header in Sidebar Layout
|
||||
"group-data-[layout=sidebar]:[&_.section-item-header>div]:flex-col",
|
||||
"group-data-[layout=sidebar]:[&_.section-item-header>div]:items-start",
|
||||
@@ -32,10 +36,18 @@ export function ChikoritaTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
|
||||
return (
|
||||
<div className="template-chikorita page-content">
|
||||
{/* Sidebar Background */}
|
||||
{!fullWidth && (
|
||||
<div className="page-sidebar-background absolute inset-y-0 right-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)" />
|
||||
)}
|
||||
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="flex">
|
||||
<main data-layout="main" className="group page-main flex-1 space-y-4 px-(--page-margin-x) py-(--page-margin-y)">
|
||||
<main
|
||||
data-layout="main"
|
||||
className="group page-main z-10 flex-1 space-y-4 px-(--page-margin-x) py-(--page-margin-y)"
|
||||
>
|
||||
{main.map((section) => {
|
||||
const Component = getSectionComponent(section, { sectionClassName });
|
||||
return <Component key={section} id={section} />;
|
||||
@@ -45,7 +57,7 @@ export function ChikoritaTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
{!fullWidth && (
|
||||
<aside
|
||||
data-layout="sidebar"
|
||||
className="group page-sidebar w-(--page-sidebar-width) shrink-0 space-y-4 overflow-x-hidden bg-(--page-primary-color) px-(--page-margin-x) pb-(--page-margin-y) text-(--page-background-color)"
|
||||
className="group page-sidebar z-10 w-(--page-sidebar-width) shrink-0 space-y-4 overflow-x-hidden px-(--page-margin-x) py-(--page-margin-y) text-(--page-background-color)"
|
||||
>
|
||||
{sidebar.map((section) => {
|
||||
const Component = getSectionComponent(section, { sectionClassName });
|
||||
@@ -111,7 +123,7 @@ function Header() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)" />
|
||||
<div className="w-(--page-sidebar-width) shrink-0" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@ const sectionClassName = cn(
|
||||
// Section Item Header in Sidebar Layout
|
||||
"group-data-[layout=sidebar]:[&_.section-item-header>div]:flex-col",
|
||||
"group-data-[layout=sidebar]:[&_.section-item-header>div]:items-start",
|
||||
|
||||
// Decoration Line in Section Item Header
|
||||
"group-data-[layout=main]:[&_.section-item-header]:pl-2",
|
||||
"group-data-[layout=main]:[&_.section-item-header]:py-0.5",
|
||||
"group-data-[layout=main]:[&_.section-item-header]:-ml-2.5",
|
||||
"group-data-[layout=main]:[&_.section-item-header]:border-l-2",
|
||||
"group-data-[layout=main]:[&_.section-item-header]:border-(--page-primary-color)",
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -27,32 +34,36 @@ export function DitgarTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="template-ditgar page-content grid min-h-[inherit] grid-cols-3">
|
||||
<div
|
||||
data-layout="sidebar"
|
||||
className={cn("sidebar group flex flex-col", !(isFirstPage || !fullWidth) && "hidden")}
|
||||
>
|
||||
{isFirstPage && <Header />}
|
||||
<div className="template-ditgar page-content">
|
||||
{/* Sidebar Background */}
|
||||
{!fullWidth && (
|
||||
<div className="page-sidebar-background absolute inset-y-0 left-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20" />
|
||||
)}
|
||||
|
||||
<div className="flex-1 space-y-4 bg-(--page-primary-color)/20 px-(--page-margin-x) py-(--page-margin-y)">
|
||||
{sidebar.map((section) => {
|
||||
const Component = getSectionComponent(section, { sectionClassName });
|
||||
return <Component key={section} id={section} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<aside data-layout="sidebar" className="sidebar group z-10 flex w-(--page-sidebar-width) shrink-0 flex-col">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div data-layout="main" className={cn("main group", !fullWidth ? "col-span-2" : "col-span-3")}>
|
||||
{isFirstPage && <SummaryComponent id="summary" />}
|
||||
|
||||
<div className="space-y-4 px-(--page-margin-x) py-(--page-margin-y)">
|
||||
{main
|
||||
.filter((section) => section !== "summary")
|
||||
.map((section) => {
|
||||
<div className="flex-1 space-y-4 px-(--page-margin-x) py-(--page-margin-y)">
|
||||
{sidebar.map((section) => {
|
||||
const Component = getSectionComponent(section, { sectionClassName });
|
||||
return <Component key={section} id={section} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main data-layout="main" className={cn("main group z-10", !fullWidth ? "col-span-2" : "col-span-3")}>
|
||||
{isFirstPage && <SummaryComponent id="summary" />}
|
||||
|
||||
<div className="space-y-4 px-(--page-margin-x) py-(--page-margin-y)">
|
||||
{main
|
||||
.filter((section) => section !== "summary")
|
||||
.map((section) => {
|
||||
const Component = getSectionComponent(section, { sectionClassName });
|
||||
return <Component key={section} id={section} />;
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,23 +25,33 @@ export function GengarTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
|
||||
return (
|
||||
<div className="template-gengar page-content">
|
||||
{/* Sidebar Background */}
|
||||
{(!fullWidth || isFirstPage) && (
|
||||
<div className="page-sidebar-background absolute inset-y-0 left-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20" />
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
<div data-layout="sidebar" className="group page-sidebar flex w-(--page-sidebar-width) shrink-0 flex-col">
|
||||
{isFirstPage && <Header />}
|
||||
{(!fullWidth || isFirstPage) && (
|
||||
<aside
|
||||
data-layout="sidebar"
|
||||
className="group page-sidebar z-10 flex w-(--page-sidebar-width) shrink-0 flex-col"
|
||||
>
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
{!fullWidth && (
|
||||
<aside className="shrink-0 space-y-4 overflow-x-hidden bg-(--page-primary-color)/20 px-(--page-margin-x) pt-4 pb-(--page-margin-y)">
|
||||
{sidebar
|
||||
.filter((section) => section !== "summary")
|
||||
.map((section) => {
|
||||
const Component = getSectionComponent(section, { sectionClassName });
|
||||
return <Component key={section} id={section} />;
|
||||
})}
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
{!fullWidth && (
|
||||
<div className="shrink-0 space-y-4 overflow-x-hidden px-(--page-margin-x) pt-4 pb-(--page-margin-y)">
|
||||
{sidebar
|
||||
.filter((section) => section !== "summary")
|
||||
.map((section) => {
|
||||
const Component = getSectionComponent(section, { sectionClassName });
|
||||
return <Component key={section} id={section} />;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
|
||||
<main data-layout="main" className="group page-main">
|
||||
<main data-layout="main" className="group page-main z-10">
|
||||
{isFirstPage && (
|
||||
<PageSummary
|
||||
className={cn(
|
||||
|
||||
@@ -25,10 +25,15 @@ export function GlalieTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
|
||||
return (
|
||||
<div className="template-glalie page-content">
|
||||
{/* Sidebar Background */}
|
||||
{(!fullWidth || isFirstPage) && (
|
||||
<div className="page-sidebar-background absolute inset-y-0 left-0 z-0 w-(--page-sidebar-width) shrink-0 bg-(--page-primary-color)/20" />
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
<aside
|
||||
data-layout="sidebar"
|
||||
className="group page-sidebar flex w-(--page-sidebar-width) shrink-0 flex-col space-y-4 bg-(--page-primary-color)/20 px-(--page-margin-x) py-(--page-margin-y)"
|
||||
className="group page-sidebar flex w-(--page-sidebar-width) shrink-0 flex-col space-y-4 px-(--page-margin-x) py-(--page-margin-y)"
|
||||
>
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
@@ -60,7 +65,7 @@ function Header() {
|
||||
|
||||
return (
|
||||
<div className="page-header relative flex">
|
||||
<div className="flex w-full shrink-0 flex-col items-center justify-center gap-y-2">
|
||||
<div className="flex w-full shrink-0 flex-col items-center justify-center gap-y-3">
|
||||
<PagePicture />
|
||||
|
||||
<div className="text-center">
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { TemplateProps } from "./types";
|
||||
|
||||
const sectionClassName = cn(
|
||||
// Container
|
||||
"rounded-(--container-border-radius) border border-(--page-text-color)/10 bg-(--page-background-color) p-4 shadow-md",
|
||||
"rounded-(--container-border-radius) border border-(--page-text-color)/10 bg-(--page-background-color) p-4",
|
||||
|
||||
// Section Heading
|
||||
"[&>h6]:-mt-(--heading-negative-margin) [&>h6]:max-w-fit [&>h6]:bg-(--page-background-color) [&>h6]:px-4 [&>h6]:pb-0.5",
|
||||
@@ -63,7 +63,7 @@ function Header() {
|
||||
<div
|
||||
className={cn(
|
||||
"page-header flex items-center gap-x-4",
|
||||
"rounded-(--picture-border-radius) border border-(--page-text-color)/10 bg-(--page-background-color) p-4 shadow-md",
|
||||
"rounded-(--picture-border-radius) border border-(--page-text-color)/10 bg-(--page-background-color) p-4",
|
||||
)}
|
||||
>
|
||||
<PagePicture />
|
||||
|
||||
@@ -11,7 +11,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
"group/input-group relative flex h-9 w-full min-w-0 items-center rounded-md border border-input shadow-xs outline-none transition-[color,box-shadow] in-data-[slot=combobox-content]:focus-within:border-ring has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-start]]:h-auto has-[>textarea]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:flex-col has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-[3px] has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
|
||||
"group/input-group relative flex h-9 w-full min-w-0 items-center rounded-md border border-input shadow-xs outline-none transition-[color,box-shadow] in-data-[slot=combobox-content]:focus-within:border-ring has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-start]]:h-auto has-[>textarea]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:flex-col has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-[3px] has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -8,7 +8,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
className="toaster group print:hidden"
|
||||
icons={{
|
||||
success: <CheckCircleIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
|
||||
Reference in New Issue
Block a user