lot of bugfixes, better migration script

This commit is contained in:
Amruth Pillai
2026-01-23 01:07:52 +01:00
parent a10954ed14
commit 35e2daa807
95 changed files with 3661 additions and 3645 deletions
+1 -6
View File
@@ -15,12 +15,7 @@ export function GithubStarsButton() {
return (
<Button asChild variant="outline">
<a
href="https://github.com/amruthpillai/reactive-resume"
target="_blank"
rel="noopener noreferrer"
aria-label={ariaLabel}
>
<a target="_blank" href="https://github.com/amruthpillai/reactive-resume" aria-label={ariaLabel} rel="noopener">
<GithubLogoIcon aria-hidden="true" />
{starCount != null ? (
<CountUp to={starCount} duration={0.5} separator="," className="font-bold" aria-hidden="true" />
+1 -5
View File
@@ -269,11 +269,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
return;
}
ctx.editor
.chain()
.focus()
.setLink({ href: url, target: "_blank", rel: "noopener noreferrer nofollow" })
.run();
ctx.editor.chain().focus().setLink({ href: url, target: "_blank", rel: "noopener nofollow" }).run();
},
unsetLink: () => ctx.editor.chain().focus().unsetLink().run(),
@@ -186,7 +186,7 @@ export function getSectionComponent(
style={{ gridTemplateColumns: `repeat(${customSection.columns}, 1fr)` }}
>
{visibleItems.map((item) => (
<div key={item.id} className={cn(`section-item section-item-${customSection.type} *:space-y-1`)}>
<div key={item.id} className={cn(`section-item section-item-${customSection.type}`)}>
{renderItemByType(customSection.type, item, itemClassName)}
</div>
))}
@@ -1,6 +1,7 @@
import { TiptapContent } from "@/components/input/rich-input";
import type { SectionItem } from "@/schema/resume/data";
import { cn } from "@/utils/style";
import { PageLink } from "../page-link";
type ReferencesItemProps = SectionItem<"references"> & {
className?: string;
@@ -9,12 +10,21 @@ type ReferencesItemProps = SectionItem<"references"> & {
export function ReferencesItem({ className, ...item }: ReferencesItemProps) {
return (
<div className={cn("references-item", className)}>
<p className="section-item-name references-item-name">
<strong>{item.name}</strong>
</p>
<div className="section-item-header references-item-header">
<div className="flex items-center justify-between">
<p className="section-item-name references-item-name">
<strong>{item.name}</strong>
</p>
</div>
<p className="section-item-subtitle references-item-position">{item.position}</p>
</div>
<div className="section-item-description references-item-description">
<TiptapContent content={item.description} />
</div>
<div className="section-item-footer references-item-footer flex flex-col">
<div className="section-item-metadata references-item-phone">{item.phone}</div>
<PageLink {...item.website} label={item.website.label} className="section-item-link references-item-link" />
</div>
</div>
);
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { LevelDisplay } from "@/components/level/display";
import { cn } from "@/utils/style";
import { useResumeStore } from "../store/resume";
type Props = React.ComponentProps<"div"> & {
@@ -8,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={className} {...props} />;
return <LevelDisplay icon={icon} type={type} level={level} className={cn("h-4", className)} {...props} />;
}
+1 -1
View File
@@ -10,7 +10,7 @@ export function PageLink({ url, label, className }: Props) {
if (!url) return null;
return (
<a href={url} target="_blank" rel="noopener noreferrer" className={cn("inline-block text-wrap", className)}>
<a href={url} target="_blank" rel="noopener" className={cn("inline-block text-wrap", className)}>
{label || url}
</a>
);
+1 -1
View File
@@ -33,7 +33,7 @@ export function LaprasTemplate({ pageIndex, pageLayout }: TemplateProps) {
"--heading-negative-margin": `${headingNegativeMargin}pt`,
} as React.CSSProperties
}
className="template-lapras page-content space-y-7 px-(--page-margin-x) py-(--page-margin-y)"
className="template-lapras page-content space-y-7 px-(--page-margin-x) py-(--page-margin-y) print:p-0"
>
{isFirstPage && <Header />}
+5 -1
View File
@@ -30,7 +30,11 @@ export function PikachuTemplate({ pageIndex, pageLayout }: TemplateProps) {
data-layout="sidebar"
className="group page-sidebar flex w-(--page-sidebar-width) shrink-0 flex-col space-y-3"
>
{isFirstPage && <PagePicture className="max-h-full! max-w-full!" />}
{isFirstPage && (
<div className="flex items-center justify-center">
<PagePicture />
</div>
)}
{!fullWidth && (
<div className="shrink-0 space-y-4 overflow-x-hidden">
+1
View File
@@ -22,6 +22,7 @@ const localFontList = [
{ type: "local", category: "serif", family: "Palatino", weights: ["400", "600", "700"] },
{ type: "local", category: "serif", family: "Times New Roman", weights: ["400", "600", "700"] },
] as LocalFont[];
const webFontList = webFontListJSON as WebFont[];
function buildWebFontMap() {
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -9,7 +9,7 @@ export function Copyright({ className, ...props }: Props) {
<p>
<Trans>
Licensed under{" "}
<a href="#" target="_blank" rel="noopener noreferrer" className="font-medium underline underline-offset-2">
<a href="#" target="_blank" rel="noopener" className="font-medium underline underline-offset-2">
MIT
</a>
.
@@ -25,7 +25,7 @@ export function Copyright({ className, ...props }: Props) {
A passion project by{" "}
<a
target="_blank"
rel="noopener noreferrer"
rel="noopener"
href="https://amruthpillai.com"
className="font-medium underline underline-offset-2"
>
+56 -1
View File
@@ -4,6 +4,7 @@ import { PencilSimpleLineIcon, PlusIcon } from "@phosphor-icons/react";
import { useForm, useFormContext } from "react-hook-form";
import type z from "zod";
import { RichInput } from "@/components/input/rich-input";
import { URLInput } from "@/components/input/url-input";
import { useResumeStore } from "@/components/resume/store/resume";
import { Button } from "@/components/ui/button";
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@@ -28,6 +29,9 @@ export function CreateReferenceDialog({ data }: DialogProps<"resume.sections.ref
id: generateId(),
hidden: data?.item?.hidden ?? false,
name: data?.item?.name ?? "",
position: data?.item?.position ?? "",
website: data?.item?.website ?? { url: "", label: "" },
phone: data?.item?.phone ?? "",
description: data?.item?.description ?? "",
},
});
@@ -83,6 +87,9 @@ export function UpdateReferenceDialog({ data }: DialogProps<"resume.sections.ref
id: data.item.id,
hidden: data.item.hidden,
name: data.item.name,
position: data.item.position,
website: data.item.website,
phone: data.item.phone,
description: data.item.description,
},
});
@@ -140,7 +147,7 @@ function ReferenceForm() {
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="sm:col-span-full">
<FormItem>
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
@@ -152,6 +159,54 @@ function ReferenceForm() {
)}
/>
<FormField
control={form.control}
name="position"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Position</Trans>
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Phone</Trans>
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="website"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Website</Trans>
</FormLabel>
<FormControl>
<URLInput {...field} value={field.value} onChange={field.onChange} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
+3
View File
@@ -584,6 +584,9 @@ export class JSONResumeImporter {
id: generateId(),
hidden: false,
name: ref.name || "",
position: "",
website: { url: "", label: "" },
phone: "",
description: ref.reference ? `<p>${ref.reference}</p>` : "",
})),
};
@@ -1,3 +1,5 @@
/** biome-ignore-all lint/style/noNonNullAssertion: only used in places where we know the value is not null */
import z, { flattenError, ZodError } from "zod";
import { type ResumeData, resumeDataSchema } from "@/schema/resume/data";
import { type Template, templateSchema } from "@/schema/templates";
@@ -122,7 +124,6 @@ type V4ResumeData = {
items: Array<{
id: string;
visible: boolean;
name?: string;
title?: string;
awarder?: string;
date?: string;
@@ -433,16 +434,16 @@ export class ReactiveResumeV4JSONImporter {
},
sections: {
profiles: {
title: (v4Data.sections.profiles?.name ?? "").trim() || "Profiles",
title: v4Data.sections.profiles?.name ?? "",
columns: v4Data.sections.profiles?.columns ?? 1,
hidden: !(v4Data.sections.profiles?.visible ?? true),
items: (v4Data.sections.profiles?.items ?? [])
.filter((item) => item.network && item.network.trim().length > 0)
.filter((item) => item.network && item.network.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
icon: item.icon ?? "",
network: item.network?.trim() || "Other",
network: item.network!,
username: item.username ?? "",
website: {
url: item.url?.href ?? "",
@@ -451,15 +452,15 @@ export class ReactiveResumeV4JSONImporter {
})),
},
experience: {
title: (v4Data.sections.experience?.name ?? "").trim() || "Experience",
title: v4Data.sections.experience?.name ?? "",
columns: v4Data.sections.experience?.columns ?? 1,
hidden: !(v4Data.sections.experience?.visible ?? true),
items: (v4Data.sections.experience?.items ?? [])
.filter((item) => item.company && item.company.trim().length > 0)
.filter((item) => item.company && item.company.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
company: item.company?.trim() || "Company",
company: item.company!,
position: item.position ?? "",
location: item.location ?? "",
period: item.date ?? "",
@@ -471,15 +472,15 @@ export class ReactiveResumeV4JSONImporter {
})),
},
education: {
title: (v4Data.sections.education?.name ?? "").trim() || "Education",
title: v4Data.sections.education?.name ?? "",
columns: v4Data.sections.education?.columns ?? 1,
hidden: !(v4Data.sections.education?.visible ?? true),
items: (v4Data.sections.education?.items ?? [])
.filter((item) => item.institution && item.institution.trim().length > 0)
.filter((item) => item.institution && item.institution.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
school: item.institution?.trim() || "School",
school: item.institution!,
degree: item.studyType ?? "",
area: item.area ?? "",
grade: item.score ?? "",
@@ -493,15 +494,15 @@ export class ReactiveResumeV4JSONImporter {
})),
},
projects: {
title: (v4Data.sections.projects?.name ?? "").trim() || "Projects",
title: v4Data.sections.projects?.name ?? "",
columns: v4Data.sections.projects?.columns ?? 1,
hidden: !(v4Data.sections.projects?.visible ?? true),
items: (v4Data.sections.projects?.items ?? [])
.filter((item) => item.name && item.name.trim().length > 0)
.filter((item) => item.name && item.name.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
name: item.name?.trim() || "Project",
name: item.name!,
period: item.date ?? "",
website: {
url: item.url?.href ?? "",
@@ -511,59 +512,59 @@ export class ReactiveResumeV4JSONImporter {
})),
},
skills: {
title: (v4Data.sections.skills?.name ?? "").trim() || "Skills",
title: v4Data.sections.skills?.name ?? "",
columns: v4Data.sections.skills?.columns ?? 1,
hidden: !(v4Data.sections.skills?.visible ?? true),
items: (v4Data.sections.skills?.items ?? [])
.filter((item) => item.name && item.name.trim().length > 0)
.filter((item) => item.name && item.name.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
icon: "",
name: item.name?.trim() || "Skill",
name: item.name!,
proficiency: item.description ?? "",
level: clampLevel(item.level ?? 0),
keywords: item.keywords ?? [],
})),
},
languages: {
title: (v4Data.sections.languages?.name ?? "").trim() || "Languages",
title: v4Data.sections.languages?.name ?? "",
columns: v4Data.sections.languages?.columns ?? 1,
hidden: !(v4Data.sections.languages?.visible ?? true),
items: (v4Data.sections.languages?.items ?? [])
.filter((item) => item.language && item.language.trim().length > 0)
.filter((item) => item.language && item.language.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
language: item.language?.trim() || "Language",
language: item.language!,
fluency: item.fluency ?? "",
level: clampLevel(item.level ?? 0),
})),
},
interests: {
title: (v4Data.sections.interests?.name ?? "").trim() || "Interests",
title: v4Data.sections.interests?.name ?? "",
columns: v4Data.sections.interests?.columns ?? 1,
hidden: !(v4Data.sections.interests?.visible ?? true),
items: (v4Data.sections.interests?.items ?? [])
.filter((item) => item.name && item.name.trim().length > 0)
.filter((item) => item.name && item.name.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
icon: "",
name: item.name?.trim() || "Interest",
name: item.name!,
keywords: item.keywords ?? [],
})),
},
awards: {
title: (v4Data.sections.awards?.name ?? "").trim() || "Awards",
title: v4Data.sections.awards?.name ?? "",
columns: v4Data.sections.awards?.columns ?? 1,
hidden: !(v4Data.sections.awards?.visible ?? true),
items: (v4Data.sections.awards?.items ?? [])
.filter((item) => (item.title || item.name) && (item.title?.trim() || item.name?.trim() || "").length > 0)
.filter((item) => item.title && item.title.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
title: (item.title || item.name)?.trim() || "Award",
title: item.title!,
awarder: item.awarder ?? "",
date: item.date ?? "",
website: {
@@ -574,15 +575,15 @@ export class ReactiveResumeV4JSONImporter {
})),
},
certifications: {
title: (v4Data.sections.certifications?.name ?? "").trim() || "Certifications",
title: v4Data.sections.certifications?.name ?? "",
columns: v4Data.sections.certifications?.columns ?? 1,
hidden: !(v4Data.sections.certifications?.visible ?? true),
items: (v4Data.sections.certifications?.items ?? [])
.filter((item) => item.name && item.name.trim().length > 0)
.filter((item) => item.name && item.name.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
title: item.name?.trim() || "Certification",
title: item.name!,
issuer: item.issuer ?? "",
date: item.date ?? "",
website: {
@@ -593,15 +594,15 @@ export class ReactiveResumeV4JSONImporter {
})),
},
publications: {
title: (v4Data.sections.publications?.name ?? "").trim() || "Publications",
title: v4Data.sections.publications?.name ?? "",
columns: v4Data.sections.publications?.columns ?? 1,
hidden: !(v4Data.sections.publications?.visible ?? true),
items: (v4Data.sections.publications?.items ?? [])
.filter((item) => item.name && item.name.trim().length > 0)
.filter((item) => item.name && item.name.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
title: item.name?.trim() || "Publication",
title: item.name!,
publisher: item.publisher ?? "",
date: item.date ?? "",
website: {
@@ -612,15 +613,15 @@ export class ReactiveResumeV4JSONImporter {
})),
},
volunteer: {
title: (v4Data.sections.volunteer?.name ?? "").trim() || "Volunteering",
title: v4Data.sections.volunteer?.name ?? "",
columns: v4Data.sections.volunteer?.columns ?? 1,
hidden: !(v4Data.sections.volunteer?.visible ?? true),
items: (v4Data.sections.volunteer?.items ?? [])
.filter((item) => item.organization && item.organization.trim().length > 0)
.filter((item) => item.organization && item.organization.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
organization: item.organization?.trim() || "Organization",
organization: item.organization!,
location: item.location ?? "",
period: item.date ?? "",
website: {
@@ -631,16 +632,22 @@ export class ReactiveResumeV4JSONImporter {
})),
},
references: {
title: (v4Data.sections.references?.name ?? "").trim() || "References",
title: v4Data.sections.references?.name ?? "",
columns: v4Data.sections.references?.columns ?? 1,
hidden: !(v4Data.sections.references?.visible ?? true),
items: (v4Data.sections.references?.items ?? [])
.filter((item) => item.name && item.name.trim().length > 0)
.filter((item) => item.name && item.name.length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
name: item.name?.trim() || "Reference",
description: item.summary ?? item.description ?? "",
name: item.name!,
position: item.description ?? "",
phone: "",
website: {
url: item.url?.href ?? "",
label: item.url?.label ?? "",
},
description: item.summary ?? "",
})),
},
},
@@ -652,10 +659,10 @@ export class ReactiveResumeV4JSONImporter {
hidden: !(section.visible ?? true),
items: section.items
.filter((item) => item.visible !== false)
.map((item) => ({
.map((item, index) => ({
id: item.id || generateId(),
hidden: !item.visible,
company: item.name?.trim() || "",
company: item.name?.trim() || `#${index + 1}`,
position: item.description ?? "",
location: item.location ?? "",
period: item.date ?? "",
+2 -2
View File
@@ -205,7 +205,7 @@ export const DonationBanner = () => (
transition={{ duration: 0.6, delay: 0.6 }}
>
<Button asChild size="lg" className="h-11 gap-2 px-6">
<a href="https://opencollective.com/reactive-resume" target="_blank" rel="noopener noreferrer">
<a href="https://opencollective.com/reactive-resume" target="_blank" rel="noopener">
<HeartIcon aria-hidden="true" weight="fill" className="text-rose-400 dark:text-rose-600" />
Open Collective
<span className="sr-only"> ({t`opens in new tab`})</span>
@@ -213,7 +213,7 @@ export const DonationBanner = () => (
</Button>
<Button asChild size="lg" className="h-11 gap-2 px-6">
<a href="https://github.com/sponsors/AmruthPillai" target="_blank" rel="noopener noreferrer">
<a href="https://github.com/sponsors/AmruthPillai" target="_blank" rel="noopener">
<GithubLogoIcon aria-hidden="true" weight="fill" className="text-zinc-400 dark:text-zinc-600" />
GitHub Sponsors
<span className="sr-only"> ({t`opens in new tab`})</span>
+1 -1
View File
@@ -36,7 +36,7 @@ const getFaqItems = (): FAQItemData[] => [
<a
href={crowdinUrl}
target="_blank"
rel="noopener noreferrer"
rel="noopener"
className={buttonVariants({ variant: "link", className: "h-auto px-0!" })}
>
contribute to the translations on Crowdin
+2 -2
View File
@@ -76,7 +76,7 @@ export function Footer() {
<a
href={social.url}
target="_blank"
rel="noopener noreferrer"
rel="noopener"
aria-label={`${social.label} (${t`opens in new tab`})`}
>
<social.icon aria-hidden="true" size={18} />
@@ -123,7 +123,7 @@ function FooterLink({ url, label }: FooterLinkItem) {
<a
href={url}
target="_blank"
rel="noopener noreferrer"
rel="noopener"
className="relative inline-block text-sm transition-colors hover:text-foreground"
>
{label}
+2 -2
View File
@@ -48,7 +48,7 @@ export function Hero() {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.8 }}
target="_blank"
rel="noopener noreferrer"
rel="noopener"
href="https://docs.rxresu.me/getting-started"
>
<Badge variant="secondary" className="h-auto gap-1.5 px-3 py-0.5">
@@ -104,7 +104,7 @@ export function Hero() {
</Button>
<Button asChild size="lg" variant="ghost" className="gap-2 px-4">
<a href="https://docs.rxresu.me" target="_blank" rel="noopener noreferrer">
<a href="https://docs.rxresu.me" target="_blank" rel="noopener">
<BookIcon aria-hidden="true" className="size-4" />
<Trans>Learn More</Trans>
<span className="sr-only">
@@ -28,7 +28,7 @@ export function ProductHuntBanner() {
return (
<a
target="_blank"
rel="noopener noreferrer"
rel="noopener"
className="flex h-8 items-center justify-center bg-secondary text-center font-medium text-[0.85rem] text-secondary-foreground tracking-tight underline-offset-2 hover:underline"
href="https://www.producthunt.com/products/reactive-resume/launches/reactive-resume-v5-2?embed=true&amp;utm_source=badge-featured&amp;utm_medium=badge&amp;utm_campaign=badge-reactive-resume-v5-2"
>
+1 -1
View File
@@ -103,7 +103,7 @@ export function Testimonials() {
<a
href={`mailto:${email}`}
target="_blank"
rel="noopener noreferrer"
rel="noopener"
className="font-medium text-foreground underline underline-offset-2 transition-colors hover:text-primary"
>
{email}
@@ -1,6 +1,5 @@
import { t } from "@lingui/core/macro";
import {
ArrowsClockwiseIcon,
CircleNotchIcon,
CubeFocusIcon,
FileJsIcon,
@@ -28,7 +27,7 @@ export function BuilderDock() {
const [_, copyToClipboard] = useCopyToClipboard();
const { data: session } = authClient.useSession();
const params = useParams({ from: "/builder/$resumeId" });
const { zoomIn, zoomOut, resetTransform, centerView } = useControls();
const { zoomIn, zoomOut, centerView } = useControls();
const { data: resume } = useQuery(orpc.resume.getById.queryOptions({ input: { id: params.resumeId } }));
const { mutateAsync: printResumeAsPDF, isPending: isPrinting } = useMutation(
@@ -40,11 +39,6 @@ export function BuilderDock() {
return `${window.location.origin}/${session.user.username}/${resume.slug}`;
}, [session, resume]);
const onReset = useCallback(() => {
resetTransform();
centerView();
}, [resetTransform, centerView]);
const onCopyUrl = useCallback(async () => {
await copyToClipboard(publicUrl);
toast.success(t`A link to your resume has been copied to clipboard.`);
@@ -79,7 +73,6 @@ export function BuilderDock() {
>
<DockIcon icon={MagnifyingGlassPlusIcon} title={t`Zoom in`} onClick={() => zoomIn(0.1)} />
<DockIcon icon={MagnifyingGlassMinusIcon} title={t`Zoom out`} onClick={() => zoomOut(0.1)} />
<DockIcon icon={ArrowsClockwiseIcon} title={t`Reset zoom`} onClick={() => onReset()} />
<DockIcon icon={CubeFocusIcon} title={t`Center view`} onClick={() => centerView()} />
<div className="mx-1 h-8 w-px bg-border" />
<DockIcon icon={LinkSimpleIcon} title={t`Copy URL`} onClick={() => onCopyUrl()} />
@@ -25,7 +25,7 @@ export function InformationSectionBuilder() {
</div>
<Button asChild size="sm" variant="default" className="mt-2 whitespace-normal px-4! text-xs">
<a href="http://opencollective.com/reactive-resume" target="_blank" rel="noopener noreferrer">
<a href="http://opencollective.com/reactive-resume" target="_blank" rel="noopener">
<HandHeartIcon />
<span className="truncate">
<Trans>Donate to Reactive Resume</Trans>
@@ -36,31 +36,31 @@ export function InformationSectionBuilder() {
<div className="flex flex-wrap gap-0.5">
<Button asChild size="sm" variant="link" className="text-xs">
<a href="https://docs.rxresu.me" target="_blank" rel="noopener noreferrer">
<a href="https://docs.rxresu.me" target="_blank" rel="noopener">
<Trans>Documentation</Trans>
</a>
</Button>
<Button asChild size="sm" variant="link" className="text-xs">
<a href="https://github.com/amruthpillai/reactive-resume" target="_blank" rel="noopener noreferrer">
<a href="https://github.com/amruthpillai/reactive-resume" target="_blank" rel="noopener">
<Trans>Source Code</Trans>
</a>
</Button>
<Button asChild size="sm" variant="link" className="text-xs">
<a href="https://github.com/amruthpillai/reactive-resume/issues" target="_blank" rel="noopener noreferrer">
<a href="https://github.com/amruthpillai/reactive-resume/issues" target="_blank" rel="noopener">
<Trans>Report a Bug</Trans>
</a>
</Button>
<Button asChild size="sm" variant="link" className="text-xs">
<a href="https://crowdin.com/project/reactive-resume" target="_blank" rel="noopener noreferrer">
<a href="https://crowdin.com/project/reactive-resume" target="_blank" rel="noopener">
<Trans>Translations</Trans>
</a>
</Button>
<Button asChild size="sm" variant="link" className="text-xs">
<a href="https://opencollective.com/reactive-resume" target="_blank" rel="noopener noreferrer">
<a href="https://opencollective.com/reactive-resume" target="_blank" rel="noopener">
<Trans>Sponsors</Trans>
</a>
</Button>
@@ -30,8 +30,8 @@ function TemplateSectionForm() {
<div className="flex @md:flex-row flex-col items-stretch gap-x-4 gap-y-2">
<Button
variant="ghost"
className="group/preview relative h-auto w-40 shrink-0 cursor-pointer p-0"
onClick={onOpenTemplateGallery}
className="group/preview relative h-auto w-40 shrink-0 cursor-pointer p-0"
>
<div className="relative z-10 aspect-page size-full overflow-hidden rounded-md opacity-100 transition-opacity group-hover/preview:opacity-50">
<img src={metadata.imageUrl} alt={metadata.name} className="size-full object-cover" />
@@ -48,9 +48,9 @@ function TemplateSectionForm() {
<p className="text-muted-foreground text-sm">{i18n.t(metadata.description)}</p>
</div>
<div className="flex flex-wrap gap-1.5">
<div className="flex flex-wrap gap-2.5">
{metadata.tags.map((tag) => (
<Badge key={tag} variant="outline">
<Badge key={tag} variant="secondary">
{tag}
</Badge>
))}
+1 -1
View File
@@ -85,7 +85,7 @@ function RouteComponent() {
</p>
<Button asChild variant="link">
<a href="https://docs.rxresu.me/api-reference" target="_blank" rel="noopener noreferrer">
<a href="https://docs.rxresu.me/api-reference" target="_blank" rel="noopener">
<LinkSimpleIcon />
<Trans>API Reference</Trans>
</a>
@@ -45,7 +45,7 @@ function RouteComponent() {
variant="link"
className="h-5 justify-start text-muted-foreground text-xs active:scale-100"
>
<a href="https://crowdin.com/project/reactive-resume" target="_blank" rel="noopener noreferrer">
<a href="https://crowdin.com/project/reactive-resume" target="_blank" rel="noopener">
<Trans>Help translate the app to your language</Trans>
<ArrowRightIcon className="size-3" />
</a>
+22 -76
View File
@@ -8,11 +8,7 @@ export const iconSchema = z
);
export const urlSchema = z.object({
url: z
.string()
.describe(
"The URL to show as a link. Must be a valid URL with a protocol (http:// or https://). Leave blank to hide.",
),
url: z.string().describe("The URL to show as a link. Must be a valid URL with a protocol (http:// or https://)."),
label: z.string().describe("The label to display for the URL. Leave blank to display the URL as-is."),
});
@@ -21,7 +17,7 @@ export const pictureSchema = z.object({
url: z
.string()
.describe(
"The URL to the picture to display on the resume. Must be a valid URL with a protocol (http:// or https://). Leave blank to hide.",
"The URL to the picture to display on the resume. Must be a valid URL with a protocol (http:// or https://).",
),
size: z
.number()
@@ -65,18 +61,14 @@ export const customFieldSchema = z.object({
id: z.string().describe("The unique identifier for the custom field. Usually generated as a UUID."),
icon: iconSchema,
text: z.string().describe("The text to display for the custom field."),
link: z
.url()
.or(z.literal(""))
.describe("If the custom field should be a link, the URL to link to. Leave blank to hide.")
.catch(""),
link: z.url().or(z.literal("")).describe("If the custom field should be a link, the URL to link to.").catch(""),
});
export const basicsSchema = z.object({
name: z.string().describe("The full name of the author of the resume."),
headline: z.string().describe("The headline of the author of the resume."),
email: z.email().or(z.literal("")).describe("The email address of the author of the resume. Leave blank to hide."),
phone: z.string().describe("The phone number of the author of the resume. Leave blank to hide."),
email: z.email().or(z.literal("")).describe("The email address of the author of the resume."),
phone: z.string().describe("The phone number of the author of the resume."),
location: z.string().describe("The location of the author of the resume."),
website: urlSchema.describe("The website of the author of the resume."),
customFields: z.array(customFieldSchema).describe("The custom fields to display on the resume."),
@@ -86,9 +78,7 @@ export const summarySchema = z.object({
title: z.string().describe("The title of the summary of the resume."),
columns: z.number().describe("The number of columns the summary should span across."),
hidden: z.boolean().describe("Whether to hide the summary from the resume."),
content: z
.string()
.describe("The content of the summary of the resume. This should be a HTML-formatted string. Leave blank to hide."),
content: z.string().describe("The content of the summary of the resume. This should be a HTML-formatted string."),
});
export const baseItemSchema = z.object({
@@ -101,9 +91,7 @@ export const awardItemSchema = baseItemSchema.extend({
awarder: z.string().describe("The awarder of the award."),
date: z.string().describe("The date when the award was received."),
website: urlSchema.describe("The website of the award, if any."),
description: z
.string()
.describe("The description of the award. This should be a HTML-formatted string. Leave blank to hide."),
description: z.string().describe("The description of the award. This should be a HTML-formatted string."),
});
export const certificationItemSchema = baseItemSchema.extend({
@@ -111,9 +99,7 @@ export const certificationItemSchema = baseItemSchema.extend({
issuer: z.string().describe("The issuer of the certification."),
date: z.string().describe("The date when the certification was received."),
website: urlSchema.describe("The website of the certification, if any."),
description: z
.string()
.describe("The description of the certification. This should be a HTML-formatted string. Leave blank to hide."),
description: z.string().describe("The description of the certification. This should be a HTML-formatted string."),
});
export const educationItemSchema = baseItemSchema.extend({
@@ -124,9 +110,7 @@ export const educationItemSchema = baseItemSchema.extend({
location: z.string().describe("The location of the school or institution."),
period: z.string().describe("The period of time the education was obtained over."),
website: urlSchema.describe("The website of the school or institution, if any."),
description: z
.string()
.describe("The description of the education. This should be a HTML-formatted string. Leave blank to hide."),
description: z.string().describe("The description of the education. This should be a HTML-formatted string."),
});
export const experienceItemSchema = baseItemSchema.extend({
@@ -135,9 +119,7 @@ export const experienceItemSchema = baseItemSchema.extend({
location: z.string().describe("The location of the company or organization."),
period: z.string().describe("The period of time the author was employed at the company or organization."),
website: urlSchema.describe("The website of the company or organization, if any."),
description: z
.string()
.describe("The description of the experience. This should be a HTML-formatted string. Leave blank to hide."),
description: z.string().describe("The description of the experience. This should be a HTML-formatted string."),
});
export const interestItemSchema = baseItemSchema.extend({
@@ -177,9 +159,7 @@ export const projectItemSchema = baseItemSchema.extend({
name: z.string().min(1).describe("The name of the project."),
period: z.string().describe("The period of time the project was worked on."),
website: urlSchema.describe("The link to the project, if any."),
description: z
.string()
.describe("The description of the project. This should be a HTML-formatted string. Leave blank to hide."),
description: z.string().describe("The description of the project. This should be a HTML-formatted string."),
});
export const publicationItemSchema = baseItemSchema.extend({
@@ -187,17 +167,18 @@ export const publicationItemSchema = baseItemSchema.extend({
publisher: z.string().describe("The publisher of the publication."),
date: z.string().describe("The date when the publication was published."),
website: urlSchema.describe("The link to the publication, if any."),
description: z
.string()
.describe("The description of the publication. This should be a HTML-formatted string. Leave blank to hide."),
description: z.string().describe("The description of the publication. This should be a HTML-formatted string."),
});
export const referenceItemSchema = baseItemSchema.extend({
name: z.string().min(1).describe("The name of the reference, or a note such as 'Available upon request'."),
position: z.string().describe("The position or job title of the reference."),
website: urlSchema.describe("The website or LinkedIn profile of the reference, if any."),
phone: z.string().describe("The phone number of the reference."),
description: z
.string()
.describe(
"The description of the reference. Can be used to display a quote, a testimonial, etc. This should be a HTML-formatted string. Leave blank to hide.",
"The description of the reference. Can be used to display a quote, a testimonial, etc. This should be a HTML-formatted string.",
),
});
@@ -230,9 +211,7 @@ export const volunteerItemSchema = baseItemSchema.extend({
website: urlSchema.describe("The link to the organization or company, if any."),
description: z
.string()
.describe(
"The description of the volunteer experience. This should be a HTML-formatted string. Leave blank to hide.",
),
.describe("The description of the volunteer experience. This should be a HTML-formatted string."),
});
export const baseSectionSchema = z.object({
@@ -842,7 +821,7 @@ export const sampleResumeData: ResumeData = {
},
skills: {
title: "Technical Skills",
columns: 2,
columns: 1,
hidden: false,
items: [
{
@@ -899,42 +878,6 @@ export const sampleResumeData: ResumeData = {
level: 4,
keywords: ["Profiling", "Memory Management", "Frame Rate"],
},
{
id: "skill7",
hidden: false,
icon: "git-branch",
name: "Version Control",
proficiency: "Advanced",
level: 5,
keywords: ["Git", "Perforce", "Plastic SCM"],
},
{
id: "skill8",
hidden: false,
icon: "devices",
name: "Platform Development",
proficiency: "Advanced",
level: 4,
keywords: ["PC", "Console (PS5/Xbox)", "Mobile", "Switch"],
},
{
id: "skill9",
hidden: false,
icon: "network",
name: "Multiplayer/Networking",
proficiency: "Intermediate",
level: 3,
keywords: ["Photon", "Mirror", "Client-Server", "P2P"],
},
{
id: "skill10",
hidden: false,
icon: "palette",
name: "Shaders & VFX",
proficiency: "Intermediate",
level: 3,
keywords: ["Shader Graph", "HLSL", "Particle Systems"],
},
],
},
languages: {
@@ -960,7 +903,7 @@ export const sampleResumeData: ResumeData = {
},
interests: {
title: "Interests",
columns: 2,
columns: 1,
hidden: false,
items: [
{
@@ -1132,6 +1075,9 @@ export const sampleResumeData: ResumeData = {
id: "ref1",
hidden: false,
name: "Available upon request",
position: "",
website: { url: "", label: "" },
phone: "",
description: "",
},
],
-303
View File
@@ -1,303 +0,0 @@
{
"$schema": "https://rxresu.me/schema.json",
"version": "5.0.0",
"picture": {
"hidden": true,
"url": "",
"size": 100,
"rotation": 0,
"aspectRatio": 1,
"borderRadius": 0,
"borderColor": "rgba(0, 0, 0, 0.5)",
"borderWidth": 0,
"shadowColor": "rgba(0, 0, 0, 0.5)",
"shadowWidth": 0
},
"basics": {
"name": "Erica Engineer, E.I.T.",
"headline": "Environmental Engineering",
"email": "EricaE@uofga.edu",
"phone": "555-555-5555",
"location": "",
"website": {
"url": "https://issuu.com/eengineersample",
"label": "issuu.com/eengineersample"
},
"customFields": []
},
"summary": {
"title": "Summary of Qualifications",
"columns": 1,
"hidden": false,
"content": "<ul><li>Experienced in designing solutions that require integration of environmental and human health constraints compatible with economic growth and sustainable development</li><li>Skilled in a variety of modeling and drafting software, including AutoCAD and ALGOR</li><li>Efficient at utilizing project management and teamwork skills to promote overarching goals</li></ul>"
},
"sections": {
"profiles": {
"title": "Profiles",
"columns": 3,
"hidden": true,
"items": []
},
"experience": {
"title": "Relevant Work Experience",
"columns": 1,
"hidden": false,
"items": [
{
"id": "a0eae67b-7e5a-4ebf-bbef-fd48ee27ffd1",
"hidden": false,
"company": "ABC Pollution Control",
"position": "Environmental Engineering Intern",
"location": "Miami, FL",
"period": "June 2022 August 2022",
"website": {
"url": "",
"label": ""
},
"description": "<ul><li>Develop 3-5 bullet points using the following formula: 1 bullet point = task + skill + quantity + result/purpose</li><li>Make sure that your most recent experience is at the top of each section, working backwards from there</li></ul>"
},
{
"id": "4114031b-ebd2-4d6f-8e47-5b1675f59d10",
"hidden": false,
"company": "Golob & Legion Engineers",
"position": "Intern",
"location": "Athens, GA",
"period": "May 2021 August 2021",
"website": {
"url": "",
"label": ""
},
"description": "<ul><li>Start each bullet point with an action verb, and make sure they are present tense for present experiences, and past tense for past experiences</li></ul>"
}
]
},
"education": {
"title": "Education",
"columns": 1,
"hidden": false,
"items": [
{
"id": "5cdf4747-beea-40c6-be86-bf3bba92f024",
"hidden": false,
"school": "University of Georgia",
"degree": "Bachelor of Science",
"area": "Environmental Engineering",
"grade": "3.75/4.00",
"location": "Athens, GA",
"period": "May 2024",
"website": {
"url": "",
"label": ""
},
"description": "<p>Engineer in Training (E.I.T.), State of Georgia, 2022<br/>Passed Fundamentals of Engineering Exam, 2022</p>"
}
]
},
"projects": {
"title": "Relevant Project Experience",
"columns": 1,
"hidden": false,
"items": [
{
"id": "6c49e60d-b0b3-4f9f-aba5-065b09371b55",
"hidden": false,
"name": "Group Project, Environmental Engineering Design Methodology",
"period": "January 2023 March 2023",
"website": {
"url": "",
"label": ""
},
"description": "<ul><li>Develop 3-5 bullet points using the following formula: 1 bullet point = task + skill + quantity + result/purpose</li><li>Ex. Joined a 5-person team to analyze existing ecosystems and conduct environmental tests using ArcGIS for presentation to organizational leadership to inform future building plans</li></ul>"
}
]
},
"skills": {
"title": "Skills",
"columns": 2,
"hidden": false,
"items": [
{
"id": "0997cf16-43d1-449e-9f96-8a5484a46131",
"hidden": false,
"icon": "star",
"name": "Technical",
"proficiency": "",
"level": 0,
"keywords": ["AutoCAD", "AutoCAD LT", "Inventor", "SAP2000", "ArcGIS", "Solidworks"]
},
{
"id": "8341a8fc-59b6-480d-9796-d7254e3920b9",
"hidden": false,
"icon": "star",
"name": "Professional",
"proficiency": "",
"level": 0,
"keywords": [
"Project management methodologies",
"Technical writing",
"Oral/Visual Presentation",
"Laboratory Procedures"
]
}
]
},
"languages": {
"title": "Languages",
"columns": 2,
"hidden": true,
"items": []
},
"interests": {
"title": "Interests",
"columns": 2,
"hidden": true,
"items": []
},
"awards": {
"title": "Awards",
"columns": 1,
"hidden": true,
"items": []
},
"certifications": {
"title": "Certifications",
"columns": 1,
"hidden": true,
"items": []
},
"publications": {
"title": "Publications",
"columns": 1,
"hidden": true,
"items": []
},
"volunteer": {
"title": "Involvement Experience",
"columns": 1,
"hidden": false,
"items": [
{
"id": "ce2871f0-5350-4545-bf30-e2b6f9d0f1a7",
"hidden": false,
"organization": "The National Association of Environmental Professionals",
"location": "Athens, GA",
"period": "January 2022 Present",
"website": {
"url": "",
"label": ""
},
"description": "<ul><li>Try to focus on how each involvement experience will make you a better engineer, and talk about what you did, not what the organization is about</li></ul>"
},
{
"id": "5b631b63-e6d1-4484-b6c4-d59ebae013fd",
"hidden": false,
"organization": "The Society of Environmental Engineers, UGA",
"location": "Athens, GA",
"period": "August 2021 Present",
"website": {
"url": "",
"label": ""
},
"description": "<ul><li>Attend weekly meetings to focus on strengthening skill sets and developing as a professional</li></ul>"
},
{
"id": "3fbd7e53-6519-437a-8ae6-53de24b34a58",
"hidden": false,
"organization": "UGA Tennis Team, Co-Captain",
"location": "Athens, GA",
"period": "August 2020 May 2023",
"website": {
"url": "",
"label": ""
},
"description": "<ul><li>Provided leadership for 8 younger athletes related to team motivation, encouragement, and success</li><li>Maintain 10 hours of training per week in addition to full course load and travel for weekend events</li></ul>"
},
{
"id": "94adc4b4-2539-415c-a271-2571031f5650",
"hidden": false,
"organization": "Sigma Pi Kappa, UGA",
"location": "Athens, GA",
"period": "December 2021 December 2022",
"website": {
"url": "",
"label": ""
},
"description": ""
}
]
},
"references": {
"title": "References",
"columns": 1,
"hidden": true,
"items": []
}
},
"customSections": [
{
"id": "fdf72428-49d2-4869-ad46-efa5cf1e2be9",
"title": "Relevant Coursework",
"columns": 1,
"hidden": false,
"content": "<p>Engineering Hydrology and Hydraulics | Sustainable Building Design | Energy and Environmental Policy Analysis | GIS for Urban Engineering, Planning, and Development | Environmental Justice: Evidence and Impact</p>"
}
],
"metadata": {
"template": "onyx",
"layout": {
"sidebarWidth": 35,
"pages": [
{
"fullWidth": false,
"main": [
"summary",
"education",
"fdf72428-49d2-4869-ad46-efa5cf1e2be9",
"projects",
"experience",
"volunteer"
],
"sidebar": ["skills"]
}
]
},
"css": {
"enabled": false,
"value": ""
},
"page": {
"gapX": 4,
"gapY": 4,
"marginX": 18,
"marginY": 18,
"format": "a4",
"locale": "en-US"
},
"design": {
"level": {
"icon": "star",
"type": "circle"
},
"colors": {
"primary": "rgba(59, 130, 246, 1)",
"text": "rgba(0, 0, 0, 1)",
"background": "rgba(255, 255, 255, 1)"
}
},
"typography": {
"body": {
"fontFamily": "IBM Plex Serif",
"fontWeights": ["400", "600"],
"fontSize": 11,
"lineHeight": 1.5
},
"heading": {
"fontFamily": "Oswald",
"fontWeights": ["600"],
"fontSize": 15,
"lineHeight": 1.5
}
},
"notes": ""
}
}