mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 01:15:26 +10:00
- Use browserless over gotenberg
- Implement functionality to move items between sections or pages - Enhance custom sections to have a `type` property - Update the v4 importer to account for custom sections - Update healthcheck to be a simple curl command - Update dependencies to latest and a lot more changes
This commit is contained in:
@@ -14,10 +14,9 @@ const buttonVariants = cva(
|
||||
accent: "bg-accent text-accent-foreground shadow-xs hover:bg-accent/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
outline: "border bg-background shadow-xs hover:bg-secondary/40 hover:text-secondary-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
ghost: "hover:bg-secondary/40 hover:text-secondary-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
|
||||
@@ -81,7 +81,7 @@ function AlertDialogContent({
|
||||
onOpenAutoFocus,
|
||||
onCloseAutoFocus,
|
||||
onEscapeKeyDown,
|
||||
transition = { type: "spring", stiffness: 150, damping: 25 },
|
||||
transition = { type: "spring", stiffness: 250, damping: 25 },
|
||||
...props
|
||||
}: AlertDialogContentProps) {
|
||||
const initialRotation = from === "bottom" || from === "left" ? "20deg" : "-20deg";
|
||||
@@ -99,6 +99,7 @@ function AlertDialogContent({
|
||||
<motion.div
|
||||
key="alert-dialog-content"
|
||||
data-slot="alert-dialog-content"
|
||||
transition={transition}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
filter: "blur(4px)",
|
||||
@@ -114,7 +115,6 @@ function AlertDialogContent({
|
||||
filter: "blur(4px)",
|
||||
transform: `perspective(500px) ${rotateAxis}(${initialRotation}) scale(0.8)`,
|
||||
}}
|
||||
transition={transition}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPrimitive.Content>
|
||||
|
||||
@@ -77,7 +77,7 @@ function DialogContent({
|
||||
onEscapeKeyDown,
|
||||
onPointerDownOutside,
|
||||
onInteractOutside,
|
||||
transition = { type: "spring", stiffness: 150, damping: 25 },
|
||||
transition = { type: "spring", stiffness: 250, damping: 25 },
|
||||
...props
|
||||
}: DialogContentProps) {
|
||||
const initialRotation = from === "bottom" || from === "left" ? "20deg" : "-20deg";
|
||||
@@ -97,6 +97,7 @@ function DialogContent({
|
||||
<motion.div
|
||||
key="dialog-content"
|
||||
data-slot="dialog-content"
|
||||
transition={transition}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
filter: "blur(4px)",
|
||||
@@ -112,7 +113,6 @@ function DialogContent({
|
||||
filter: "blur(4px)",
|
||||
transform: `perspective(500px) ${rotateAxis}(${initialRotation}) scale(0.8)`,
|
||||
}}
|
||||
transition={transition}
|
||||
{...props}
|
||||
/>
|
||||
</DialogPrimitive.Content>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { match } from "ts-pattern";
|
||||
import type { SectionType } from "@/schema/resume/data";
|
||||
import type { CustomSectionItem, SectionType } from "@/schema/resume/data";
|
||||
import { cn } from "@/utils/style";
|
||||
import { useResumeStore } from "../store/resume";
|
||||
import { AwardsItem } from "./items/awards-item";
|
||||
import { CertificationsItem } from "./items/certifications-item";
|
||||
import { EducationItem } from "./items/education-item";
|
||||
@@ -12,7 +14,6 @@ import { PublicationsItem } from "./items/publications-item";
|
||||
import { ReferencesItem } from "./items/references-item";
|
||||
import { SkillsItem } from "./items/skills-item";
|
||||
import { VolunteerItem } from "./items/volunteer-item";
|
||||
import { PageCustomSection } from "./page-custom";
|
||||
import { PageSection } from "./page-section";
|
||||
import { PageSummary } from "./page-summary";
|
||||
|
||||
@@ -21,6 +22,44 @@ type SectionComponentProps = {
|
||||
itemClassName?: string;
|
||||
};
|
||||
|
||||
// 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("certifications", () => (
|
||||
<CertificationsItem {...(item as Parameters<typeof CertificationsItem>[0])} 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} />
|
||||
))
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
export function getSectionComponent(
|
||||
section: "summary" | SectionType | (string & {}),
|
||||
{ sectionClassName, itemClassName }: SectionComponentProps = {},
|
||||
@@ -127,9 +166,39 @@ export function getSectionComponent(
|
||||
return ReferencesSection;
|
||||
})
|
||||
.otherwise(() => {
|
||||
const CustomSection = ({ id }: { id: string }) => (
|
||||
<PageCustomSection sectionId={id} className={sectionClassName} />
|
||||
);
|
||||
return CustomSection;
|
||||
// Custom section - render based on its type
|
||||
const CustomSectionComponent = ({ id }: { id: string }) => {
|
||||
const customSection = useResumeStore((state) => state.resume.data.customSections.find((s) => s.id === id));
|
||||
|
||||
if (!customSection) return null;
|
||||
if (customSection.hidden) return null;
|
||||
if (customSection.items.length === 0) return null;
|
||||
|
||||
const visibleItems = customSection.items.filter((item) => !item.hidden);
|
||||
if (visibleItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(`page-section page-section-custom page-section-${id} wrap-break-word`, sectionClassName)}
|
||||
>
|
||||
<h6 className="mb-1 text-(--page-primary-color)">{customSection.title}</h6>
|
||||
|
||||
<div
|
||||
className="section-content grid gap-x-(--page-gap-x) gap-y-(--page-gap-y)"
|
||||
style={{ gridTemplateColumns: `repeat(${customSection.columns}, 1fr)` }}
|
||||
>
|
||||
{visibleItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(`section-item section-item-${customSection.type} wrap-break-word *:space-y-1`)}
|
||||
>
|
||||
{renderItemByType(customSection.type, item, itemClassName)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
return CustomSectionComponent;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { TiptapContent } from "@/components/input/rich-input";
|
||||
import { cn } from "@/utils/style";
|
||||
import { useResumeStore } from "../store/resume";
|
||||
|
||||
type PageCustomSectionProps = {
|
||||
sectionId: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function PageCustomSection({ sectionId, className }: PageCustomSectionProps) {
|
||||
const section = useResumeStore((state) =>
|
||||
state.resume.data.customSections.find((section) => section.id === sectionId),
|
||||
);
|
||||
|
||||
// biome-ignore lint/complexity/noUselessFragments: render empty fragment, instead of null
|
||||
if (!section) return <></>;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
`page-section page-section-custom page-section-${sectionId} wrap-break-word`,
|
||||
section.hidden && "hidden",
|
||||
section.content === "" && "hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<h6 className="mb-2 text-(--page-primary-color)">{section.title}</h6>
|
||||
|
||||
<div className="section-content">
|
||||
<TiptapContent style={{ columnCount: section.columns }} content={section.content} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export function AzurillTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
const { main, sidebar, fullWidth } = pageLayout;
|
||||
|
||||
return (
|
||||
<div className="template-azurill page-content space-y-(--page-gap-y) px-(--page-margin-x) py-(--page-margin-y)">
|
||||
<div className="template-azurill page-content space-y-(--page-gap-y) px-(--page-margin-x) py-(--page-margin-y) print:p-0">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="flex gap-x-(--page-gap-x)">
|
||||
|
||||
@@ -23,7 +23,7 @@ export function BronzorTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
const { main, sidebar, fullWidth } = pageLayout;
|
||||
|
||||
return (
|
||||
<div className="template-bronzor page-content space-y-(--page-gap-y) px-(--page-margin-x) py-(--page-margin-y)">
|
||||
<div className="template-bronzor page-content space-y-(--page-gap-y) px-(--page-margin-x) py-(--page-margin-y) print:p-0">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div className="space-y-(--page-gap-y)">
|
||||
|
||||
@@ -20,7 +20,7 @@ export function KakunaTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
const { main, sidebar, fullWidth } = pageLayout;
|
||||
|
||||
return (
|
||||
<div className="template-kakuna page-content space-y-4 px-(--page-margin-x) py-(--page-margin-y)">
|
||||
<div className="template-kakuna page-content space-y-4 px-(--page-margin-x) py-(--page-margin-y) print:p-0">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<main data-layout="main" className="group page-main space-y-4">
|
||||
|
||||
@@ -17,7 +17,7 @@ export function OnyxTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
const { main, sidebar, fullWidth } = pageLayout;
|
||||
|
||||
return (
|
||||
<div className="template-onyx page-content space-y-(--page-gap-y) px-(--page-margin-x) py-(--page-margin-y)">
|
||||
<div className="template-onyx page-content space-y-(--page-gap-y) px-(--page-margin-x) py-(--page-margin-y) print:p-0">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<main data-layout="main" className="group page-main space-y-(--page-gap-y)">
|
||||
|
||||
@@ -24,7 +24,7 @@ export function PikachuTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
const { main, sidebar, fullWidth } = pageLayout;
|
||||
|
||||
return (
|
||||
<div className="template-pikachu page-content px-(--page-margin-x) py-(--page-margin-y)">
|
||||
<div className="template-pikachu page-content px-(--page-margin-x) py-(--page-margin-y) print:p-0">
|
||||
<div className="flex gap-x-(--page-margin-x)">
|
||||
<aside
|
||||
data-layout="sidebar"
|
||||
|
||||
@@ -20,7 +20,7 @@ export function RhyhornTemplate({ pageIndex, pageLayout }: TemplateProps) {
|
||||
const { main, sidebar, fullWidth } = pageLayout;
|
||||
|
||||
return (
|
||||
<div className="template-rhyhorn page-content space-y-4 px-(--page-margin-x) py-(--page-margin-y)">
|
||||
<div className="template-rhyhorn page-content space-y-4 px-(--page-margin-x) py-(--page-margin-y) print:p-0">
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<main data-layout="main" className="group page-main space-y-4">
|
||||
|
||||
@@ -109,7 +109,7 @@ function InputGroupInput({ className, ...props }: React.ComponentProps<"input">)
|
||||
<Input
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"flex-1 rounded-none border-0 bg-transparent shadow-none aria-invalid:ring-0 dark:bg-transparent",
|
||||
"flex-1 rounded-none border-0 bg-transparent shadow-none hover:bg-secondary/40 focus-visible:bg-secondary/40 aria-invalid:ring-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -7,7 +7,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-2.5 py-1 shadow-xs outline-none transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-2.5 py-1 shadow-xs outline-none transition-[color,box-shadow] file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground placeholder:text-muted-foreground hover:bg-secondary/40 focus-visible:border-ring focus-visible:bg-secondary/40 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user