design nosepass template, add tests, add template previews

This commit is contained in:
Amruth Pillai
2023-11-17 08:31:12 +01:00
parent 0b4cb71320
commit 34247f13b6
92 changed files with 24440 additions and 35518 deletions

View File

@ -1,20 +1,21 @@
export const colors: string[] = [
"#78716c", // stone-500
"#ef4444", // red-500
"#f97316", // orange-500
"#f59e0b", // amber-500
"#eab308", // yellow-500
"#84cc16", // lime-500
"#22c55e", // green-500
"#10b981", // emerald-500
"#14b8a6", // teal-500
"#06b6d4", // cyan-500
"#0ea5e9", // sky-500
"#3b82f6", // blue-500
"#6366f1", // indigo-500
"#8b5cf6", // violet-500
"#a855f7", // purple-500
"#d946ef", // fuchsia-500
"#ec4899", // pink-500
"#f43f5e", // rose-500
"#475569", // slate-600
"#57534e", // stone-600
"#dc2626", // red-600
"#ea580c", // orange-600
"#d97706", // amber-600
"#ca8a04", // yellow-600
"#65a30d", // lime-600
"#16a34a", // green-600
"#059669", // emerald-600
"#0d9488", // teal-600
"#0891b2", // cyan-600
"#0284c7", // sky-600
"#2563eb", // blue-600
"#4f46e5", // indigo-600
"#7c3aed", // violet-600
"#9333ea", // purple-600
"#c026d3", // fuchsia-600
"#db2777", // pink-600
"#e11d48", // rose-600
];

View File

@ -210,7 +210,7 @@ msgid "Backup Codes may contain only lowercase letters or numbers, and must be e
msgstr "Backup Codes may contain only lowercase letters or numbers, and must be exactly 10 characters."
#: apps/client/src/pages/builder/sidebars/left/index.tsx:57
msgctxt "The Basics section of a Resume consists of User's Picture, Full Name, Location etc."
msgctxt "The basics section of a resume consists of User's Picture, Full Name, Location etc."
msgid "Basics"
msgstr "Basics"
@ -247,7 +247,7 @@ msgstr "Cancel"
msgid "Casual"
msgstr "Casual"
#: apps/client/src/pages/builder/_components/toolbar.tsx:90
#: apps/client/src/pages/builder/_components/toolbar.tsx:89
msgid "Center Artboard"
msgstr "Center Artboard"
@ -310,7 +310,7 @@ msgstr "Continue"
msgid "Copy"
msgstr "Copy"
#: apps/client/src/pages/builder/_components/toolbar.tsx:124
#: apps/client/src/pages/builder/_components/toolbar.tsx:123
msgid "Copy Link to Resume"
msgstr "Copy Link to Resume"
@ -521,7 +521,7 @@ msgstr "Even if you're not in a position to contribute financially, you can stil
msgid "Export"
msgstr "Export"
#: apps/client/src/pages/builder/_components/toolbar.tsx:130
#: apps/client/src/pages/builder/_components/toolbar.tsx:129
msgid "Export as PDF"
msgstr "Export as PDF"
@ -652,6 +652,8 @@ msgstr "Here, you can update your account information such as your profile pictu
msgid "Here, you can update your profile to customize and personalize your experience."
msgstr "Here, you can update your profile to customize and personalize your experience."
#: apps/client/src/pages/builder/sidebars/left/dialogs/languages.tsx:77
#: apps/client/src/pages/builder/sidebars/left/dialogs/skills.tsx:82
#: apps/client/src/pages/builder/sidebars/left/sections/picture/options.tsx:185
msgid "Hidden"
msgstr "Hidden"
@ -787,7 +789,7 @@ msgstr "JSON"
#: apps/client/src/pages/builder/sidebars/left/dialogs/custom-section.tsx:145
#: apps/client/src/pages/builder/sidebars/left/dialogs/interests.tsx:55
#: apps/client/src/pages/builder/sidebars/left/dialogs/projects.tsx:122
#: apps/client/src/pages/builder/sidebars/left/dialogs/skills.tsx:95
#: apps/client/src/pages/builder/sidebars/left/dialogs/skills.tsx:99
msgid "Keywords"
msgstr "Keywords"
@ -952,7 +954,7 @@ msgstr "Notes"
msgid "One-Time Password"
msgstr "One-Time Password"
#: apps/client/src/libs/axios.ts:32
#: apps/client/src/libs/axios.ts:34
#: apps/client/src/pages/dashboard/resumes/_dialogs/import.tsx:190
msgid "Oops, the server returned an error."
msgstr "Oops, the server returned an error."
@ -1028,7 +1030,7 @@ msgstr "Photograph by Patrick Tomasso"
msgid "Pick any font from Google Fonts"
msgstr "Pick any font from Google Fonts"
#: apps/client/src/pages/builder/sidebars/left/sections/picture/section.tsx:52
#: apps/client/src/pages/builder/sidebars/left/sections/picture/section.tsx:69
#: apps/client/src/pages/dashboard/settings/_sections/account.tsx:120
msgid "Picture"
msgstr "Picture"
@ -1118,7 +1120,7 @@ msgstr "Reactive Resume is a passion project of over 3 years of hard work, and w
msgid "Reactive Resume thrives thanks to its vibrant community. This project owes its progress to numerous individuals who've dedicated their time and skills. Below, we celebrate the coders who've enhanced its features on GitHub and the linguists whose translations on Crowdin have made it accessible to a broader audience."
msgstr "Reactive Resume thrives thanks to its vibrant community. This project owes its progress to numerous individuals who've dedicated their time and skills. Below, we celebrate the coders who've enhanced its features on GitHub and the linguists whose translations on Crowdin have made it accessible to a broader audience."
#: apps/client/src/pages/builder/_components/toolbar.tsx:64
#: apps/client/src/pages/builder/_components/toolbar.tsx:63
msgid "Redo"
msgstr "Redo"
@ -1150,7 +1152,7 @@ msgstr "Reset Layout"
msgid "Reset your password"
msgstr "Reset your password"
#: apps/client/src/pages/builder/_components/toolbar.tsx:84
#: apps/client/src/pages/builder/_components/toolbar.tsx:83
msgid "Reset Zoom"
msgstr "Reset Zoom"
@ -1444,11 +1446,11 @@ msgstr "Title"
msgid "Title"
msgstr "Title"
#: apps/client/src/pages/builder/_components/toolbar.tsx:98
#: apps/client/src/pages/builder/_components/toolbar.tsx:97
msgid "Toggle Page Break Line"
msgstr "Toggle Page Break Line"
#: apps/client/src/pages/builder/_components/toolbar.tsx:110
#: apps/client/src/pages/builder/_components/toolbar.tsx:109
msgid "Toggle Page Numbers"
msgstr "Toggle Page Numbers"
@ -1487,7 +1489,7 @@ msgstr "Typography"
msgid "Underline Links"
msgstr "Underline Links"
#: apps/client/src/pages/builder/_components/toolbar.tsx:58
#: apps/client/src/pages/builder/_components/toolbar.tsx:57
msgid "Undo"
msgstr "Undo"
@ -1603,7 +1605,7 @@ msgstr "What's new in the latest version"
#: apps/client/src/pages/builder/sidebars/left/dialogs/custom-section.tsx:150
#: apps/client/src/pages/builder/sidebars/left/dialogs/interests.tsx:60
#: apps/client/src/pages/builder/sidebars/left/dialogs/projects.tsx:127
#: apps/client/src/pages/builder/sidebars/left/dialogs/skills.tsx:100
#: apps/client/src/pages/builder/sidebars/left/dialogs/skills.tsx:104
msgid "You can add multiple keywords by separating them with a comma or pressing enter."
msgstr "You can add multiple keywords by separating them with a comma or pressing enter."
@ -1651,10 +1653,10 @@ msgstr "Your OpenAI API Key has not been set yet. Please go to your account sett
msgid "Your password has been updated successfully."
msgstr "Your password has been updated successfully."
#: apps/client/src/pages/builder/_components/toolbar.tsx:72
#: apps/client/src/pages/builder/_components/toolbar.tsx:71
msgid "Zoom In"
msgstr "Zoom In"
#: apps/client/src/pages/builder/_components/toolbar.tsx:78
#: apps/client/src/pages/builder/_components/toolbar.tsx:77
msgid "Zoom Out"
msgstr "Zoom Out"

View File

@ -5,7 +5,7 @@ import {
CircleNotch,
ClockClockwise,
CubeFocus,
DownloadSimple,
FilePdf,
Hash,
LineSegment,
LinkSimple,
@ -51,7 +51,7 @@ export const BuilderToolbar = () => {
<motion.div
initial={{ opacity: 0.5 }}
whileHover={{ opacity: 1 }}
className="fixed inset-x-0 mx-auto hidden pb-4 pt-6 text-center md:block"
className="fixed inset-x-0 bottom-0 mx-auto pb-4 pt-6 text-center md:block"
>
<div className="inline-flex items-center justify-center rounded-full bg-background px-4 shadow-xl">
<Tooltip content={t`Undo`}>
@ -134,7 +134,7 @@ export const BuilderToolbar = () => {
onClick={onPrint}
disabled={loading}
>
{loading ? <CircleNotch className="animate-spin" /> : <DownloadSimple />}
{loading ? <CircleNotch className="animate-spin" /> : <FilePdf />}
</Button>
</Tooltip>
</div>

View File

@ -10,7 +10,6 @@ import {
Input,
Slider,
} from "@reactive-resume/ui";
import { getCEFRLevel } from "@reactive-resume/utils";
import { useForm } from "react-hook-form";
import { z } from "zod";
@ -44,11 +43,11 @@ export const LanguagesDialog = () => {
/>
<FormField
name="fluency"
name="description"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>{t`Fluency`}</FormLabel>
<FormLabel>{t`Description`}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@ -58,22 +57,26 @@ export const LanguagesDialog = () => {
/>
<FormField
name="fluencyLevel"
name="level"
control={form.control}
render={({ field }) => (
<FormItem className="sm:col-span-2">
<FormLabel>{t`Fluency (CEFR)`}</FormLabel>
<FormLabel>{t`Level`}</FormLabel>
<FormControl className="py-2">
<div className="flex items-center gap-x-4">
<Slider
{...field}
min={1}
max={6}
min={0}
max={5}
value={[field.value]}
onValueChange={(value) => field.onChange(value[0])}
/>
<span className="text-base font-bold">{getCEFRLevel(field.value)}</span>
{field.value === 0 ? (
<span className="text-base font-bold">{t`Hidden`}</span>
) : (
<span className="text-base font-bold">{field.value}</span>
)}
</div>
</FormControl>
<FormMessage />

View File

@ -71,14 +71,18 @@ export const SkillsDialog = () => {
<div className="flex items-center gap-x-4">
<Slider
{...field}
min={1}
min={0}
max={5}
value={[field.value]}
orientation="horizontal"
onValueChange={(value) => field.onChange(value[0])}
/>
<span className="text-base font-bold">{field.value}</span>
{field.value === 0 ? (
<span className="text-base font-bold">{t`Hidden`}</span>
) : (
<span className="text-base font-bold">{field.value}</span>
)}
</div>
</FormControl>
<FormMessage />

View File

@ -16,7 +16,6 @@ import {
Volunteer,
} from "@reactive-resume/schema";
import { Button, ScrollArea, Separator } from "@reactive-resume/ui";
import { getCEFRLevel } from "@reactive-resume/utils";
import { Fragment, useRef } from "react";
import { Link } from "react-router-dom";
@ -57,21 +56,21 @@ export const LeftSidebar = () => {
name={t({
message: "Basics",
context:
"The Basics section of a Resume consists of User's Picture, Full Name, Location etc.",
"The basics section of a resume consists of User's Picture, Full Name, Location etc.",
})}
/>
<SectionIcon id="summary" onClick={() => scrollIntoView("#summary")} />
<SectionIcon id="profiles" onClick={() => scrollIntoView("#profiles")} />
<SectionIcon id="experience" onClick={() => scrollIntoView("#experience")} />
<SectionIcon id="education" onClick={() => scrollIntoView("#education")} />
<SectionIcon id="skills" onClick={() => scrollIntoView("#skills")} />
<SectionIcon id="languages" onClick={() => scrollIntoView("#languages")} />
<SectionIcon id="awards" onClick={() => scrollIntoView("#awards")} />
<SectionIcon id="certifications" onClick={() => scrollIntoView("#certifications")} />
<SectionIcon id="interests" onClick={() => scrollIntoView("#interests")} />
<SectionIcon id="languages" onClick={() => scrollIntoView("#languages")} />
<SectionIcon id="volunteer" onClick={() => scrollIntoView("#volunteer")} />
<SectionIcon id="projects" onClick={() => scrollIntoView("#projects")} />
<SectionIcon id="publications" onClick={() => scrollIntoView("#publications")} />
<SectionIcon id="skills" onClick={() => scrollIntoView("#skills")} />
<SectionIcon id="volunteer" onClick={() => scrollIntoView("#volunteer")} />
<SectionIcon id="references" onClick={() => scrollIntoView("#references")} />
<SectionIcon
@ -118,6 +117,21 @@ export const LeftSidebar = () => {
description={(item) => item.area}
/>
<Separator />
<SectionBase<Skill>
id="skills"
title={(item) => item.name}
description={(item) => {
if (item.description) return item.description;
if (item.keywords.length > 0) return `${item.keywords.length} keywords`;
}}
/>
<Separator />
<SectionBase<Language>
id="languages"
title={(item) => item.name}
description={(item) => item.description}
/>
<Separator />
<SectionBase<Award>
id="awards"
title={(item) => item.title}
@ -138,18 +152,6 @@ export const LeftSidebar = () => {
}}
/>
<Separator />
<SectionBase<Language>
id="languages"
title={(item) => item.name}
description={(item) => item.fluency || getCEFRLevel(item.fluencyLevel)}
/>
<Separator />
<SectionBase<Volunteer>
id="volunteer"
title={(item) => item.organization}
description={(item) => item.position}
/>
<Separator />
<SectionBase<Project>
id="projects"
title={(item) => item.name}
@ -162,13 +164,10 @@ export const LeftSidebar = () => {
description={(item) => item.publisher}
/>
<Separator />
<SectionBase<Skill>
id="skills"
title={(item) => item.name}
description={(item) => {
if (item.description) return item.description;
if (item.keywords.length > 0) return `${item.keywords.length} keywords`;
}}
<SectionBase<Volunteer>
id="volunteer"
title={(item) => item.organization}
description={(item) => item.position}
/>
<Separator />
<SectionBase<Reference>

View File

@ -1,8 +1,7 @@
import { t } from "@lingui/macro";
import { Aperture, UploadSimple } from "@phosphor-icons/react";
import { Aperture, Trash, UploadSimple } from "@phosphor-icons/react";
import {
Avatar,
AvatarFallback,
AvatarImage,
buttonVariants,
Input,
@ -11,8 +10,8 @@ import {
PopoverContent,
PopoverTrigger,
} from "@reactive-resume/ui";
import { cn, getInitials } from "@reactive-resume/utils";
import { AnimatePresence, motion } from "framer-motion";
import { cn } from "@reactive-resume/utils";
import { motion } from "framer-motion";
import { useMemo, useRef } from "react";
import { z } from "zod";
@ -23,10 +22,9 @@ import { PictureOptions } from "./options";
export const PictureSection = () => {
const inputRef = useRef<HTMLInputElement>(null);
const { uploadImage, loading } = useUploadImage();
const { uploadImage } = useUploadImage();
const setValue = useResumeStore((state) => state.setValue);
const name = useResumeStore((state) => state.resume.data.basics.name);
const picture = useResumeStore((state) => state.resume.data.basics.picture);
const isValidUrl = useMemo(() => z.string().url().safeParse(picture.url).success, [picture.url]);
@ -41,16 +39,37 @@ export const PictureSection = () => {
}
};
const onAvatarClick = () => {
if (isValidUrl) {
setValue("basics.picture.url", "");
} else {
inputRef.current?.click();
}
};
return (
<div className="flex items-center gap-x-4">
<Avatar className="h-14 w-14">
{isValidUrl && <AvatarImage src={picture.url} />}
<AvatarFallback className="text-lg font-bold">{getInitials(name)}</AvatarFallback>
</Avatar>
<div className="group relative cursor-pointer" onClick={onAvatarClick}>
<Avatar className="h-14 w-14 bg-secondary">
<AvatarImage src={picture.url} />
</Avatar>
{isValidUrl ? (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-full bg-background/30 opacity-0 transition-opacity group-hover:opacity-100">
<Trash size={16} weight="bold" />
</div>
) : (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-full bg-background/30 opacity-0 transition-opacity group-hover:opacity-100">
<UploadSimple size={16} weight="bold" />
</div>
)}
</div>
<div className="flex w-full flex-col gap-y-1.5">
<Label htmlFor="basics.picture.url">{t`Picture`}</Label>
<div className="flex items-center gap-x-2">
<input hidden type="file" ref={inputRef} onChange={onSelectImage} />
<Input
id="basics.picture.url"
placeholder="https://..."
@ -58,44 +77,23 @@ export const PictureSection = () => {
onChange={(event) => setValue("basics.picture.url", event.target.value)}
/>
<AnimatePresence>
{/* Show options button if picture exists */}
{isValidUrl && (
<Popover>
<PopoverTrigger asChild>
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={cn(buttonVariants({ size: "icon", variant: "ghost" }))}
>
<Aperture />
</motion.button>
</PopoverTrigger>
<PopoverContent className="w-[360px]">
<PictureOptions />
</PopoverContent>
</Popover>
)}
{/* Show upload button if picture doesn't exist, else show remove button to delete picture */}
{!isValidUrl && (
<>
<input hidden type="file" ref={inputRef} onChange={onSelectImage} />
{isValidUrl && (
<Popover>
<PopoverTrigger asChild>
<motion.button
disabled={loading}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => inputRef.current?.click()}
className={cn(buttonVariants({ size: "icon", variant: "ghost" }))}
>
<UploadSimple />
<Aperture />
</motion.button>
</>
)}
</AnimatePresence>
</PopoverTrigger>
<PopoverContent className="w-[360px]">
<PictureOptions />
</PopoverContent>
</Popover>
)}
</div>
</div>
</div>

View File

@ -1,6 +1,7 @@
import { t } from "@lingui/macro";
import { Button, HoverCard, HoverCardContent, HoverCardTrigger } from "@reactive-resume/ui";
import { AspectRatio } from "@reactive-resume/ui";
import { cn, templatesList } from "@reactive-resume/utils";
import { motion } from "framer-motion";
import { useResumeStore } from "@/client/stores/resume";
@ -19,31 +20,28 @@ export const TemplateSection = () => {
</div>
</header>
<main className="grid grid-cols-2 gap-4">
{templatesList.map(({ id, name }) => (
<HoverCard key={id} openDelay={0} closeDelay={0}>
<HoverCardTrigger asChild>
<Button
variant="outline"
onClick={() => setValue("metadata.template", id)}
className={cn(
"flex h-12 items-center justify-center overflow-hidden rounded border text-center text-sm capitalize ring-primary transition-colors hover:bg-secondary-accent focus:outline-none focus:ring-1 disabled:opacity-100",
id === currentTemplate && "ring-1",
)}
>
{name}
</Button>
</HoverCardTrigger>
<main className="grid grid-cols-2 gap-5 @lg/right:grid-cols-3 @2xl/right:grid-cols-4">
{templatesList.map((template, index) => (
<AspectRatio ratio={1 / 1.4142}>
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0, transition: { delay: index * 0.1 } }}
whileTap={{ scale: 0.98, transition: { duration: 0.1 } }}
onClick={() => setValue("metadata.template", template)}
className={cn(
"relative cursor-pointer rounded-sm ring-primary transition-all hover:ring-2",
currentTemplate === template && "ring-2",
)}
>
<img src={`/templates/jpg/${template}.jpg`} alt={template} className="rounded-sm" />
<HoverCardContent className="max-w-xs overflow-hidden border-none bg-white p-0">
<img
alt={name}
loading="lazy"
src="/templates/sample.jpg"
className="aspect-[1/1.4142]"
/>
</HoverCardContent>
</HoverCard>
<div className="absolute inset-x-0 bottom-0 h-32 w-full bg-gradient-to-b from-transparent to-background/80">
<p className="absolute inset-x-0 bottom-2 text-center font-bold capitalize text-primary">
{template}
</p>
</div>
</motion.div>
</AspectRatio>
))}
</main>
</section>

View File

@ -137,7 +137,7 @@ export const TypographySection = () => {
<Slider
min={12}
max={18}
step={1}
step={0.05}
value={[typography.font.size]}
onValueChange={(value) => {
setValue("metadata.typography.font.size", value[0]);
@ -154,7 +154,7 @@ export const TypographySection = () => {
<Slider
min={0}
max={3}
step={0.25}
step={0.05}
value={[typography.lineHeight]}
onValueChange={(value) => {
setValue("metadata.typography.lineHeight", value[0]);

View File

@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { Check, DownloadSimple, Warning } from "@phosphor-icons/react";
import { Check, DownloadSimple } from "@phosphor-icons/react";
import {
JsonResume,
JsonResumeParser,
@ -141,7 +141,6 @@ export const ImportDialog = () => {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: t`An error occurred while validating the file.`,
});
}
@ -186,7 +185,6 @@ export const ImportDialog = () => {
} catch (error) {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: t`Oops, the server returned an error.`,
description: importError?.message,
});

View File

@ -6,9 +6,9 @@ import { FAQSection } from "./sections/faq";
import { FeaturesSection } from "./sections/features";
import { HeroSection } from "./sections/hero";
import { LogoCloudSection } from "./sections/logo-cloud";
import { SampleResumesSection } from "./sections/sample-resumes";
import { StatisticsSection } from "./sections/statistics";
import { SupportSection } from "./sections/support";
import { TemplatesSection } from "./sections/templates";
import { TestimonialsSection } from "./sections/testimonials";
export const HomePage = () => (
@ -23,7 +23,7 @@ export const HomePage = () => (
<LogoCloudSection />
<StatisticsSection />
<FeaturesSection />
<SampleResumesSection />
<TemplatesSection />
<TestimonialsSection />
<SupportSection />
<FAQSection />

View File

@ -1,35 +1,22 @@
import { t, Trans } from "@lingui/macro";
import { t } from "@lingui/macro";
import { templatesList } from "@reactive-resume/utils";
import { motion } from "framer-motion";
const resumes = [
"/sample-resumes/ditto",
"/sample-resumes/ditto",
"/sample-resumes/ditto",
"/sample-resumes/ditto",
];
export const SampleResumesSection = () => (
export const TemplatesSection = () => (
<section id="sample-resumes" className="relative py-24 sm:py-32">
<div className="container flex flex-col gap-12 lg:min-h-[600px] lg:flex-row lg:items-start">
<div className="space-y-4 lg:mt-16 lg:basis-96">
<h2 className="text-4xl font-bold">{t`Sample Resumes`}</h2>
<h2 className="text-4xl font-bold">{t`Templates`}</h2>
<Trans>
<p className="leading-relaxed">
Have a look at some of the resume created to showcase the templates available on
Reactive Resume.
</p>
<p className="leading-relaxed">
They could also serve as examples to help guide the creation of your next resume.
</p>
</Trans>
<p className="leading-relaxed">
{t`Explore the templates available in Reactive Resume and view the resumes crafted with them. They could also serve as examples to help guide the creation of your next resume.`}
</p>
</div>
<div className="w-full overflow-hidden lg:absolute lg:right-0 lg:max-w-[55%]">
<motion.div
animate={{
x: [0, -400],
x: [0, templatesList.length * 200 * -1],
transition: {
x: {
duration: 30,
@ -40,20 +27,21 @@ export const SampleResumesSection = () => (
}}
className="flex items-center gap-x-6"
>
{resumes.map((resume, index) => (
{templatesList.map((template, index) => (
<motion.a
key={index}
target="_blank"
rel="noreferrer"
href={`${resume}.pdf`}
href={`templates/pdf/${template}.pdf`}
className="max-w-none flex-none"
viewport={{ once: true }}
initial={{ opacity: 0, x: -100 }}
whileInView={{ opacity: 1, x: 0, transition: { delay: (index + 1) * 0.5 } }}
whileInView={{ opacity: 1, x: 0 }}
>
<img
alt={resume}
src={`${resume}.jpg`}
alt={template}
loading="lazy"
src={`/templates/jpg/${template}.jpg`}
className=" aspect-[1/1.4142] h-[400px] rounded object-cover lg:h-[600px]"
/>
</motion.a>

View File

@ -1,7 +1,9 @@
import { t } from "@lingui/macro";
import { StatisticsDto, UrlDto } from "@reactive-resume/dto";
import { useMutation } from "@tanstack/react-query";
import { RESUME_KEY } from "@/client/constants/query-keys";
import { toast } from "@/client/hooks/use-toast";
import { axios } from "@/client/libs/axios";
import { queryClient } from "@/client/libs/query-client";
@ -24,6 +26,15 @@ export const usePrintResume = () => {
return { ...cache, downloads: cache.downloads + 1 } satisfies StatisticsDto;
});
},
onError: (error) => {
const message = error?.message;
toast({
variant: "error",
title: t`Oops, the server returned an error.`,
description: message,
});
},
});
return { printResume: printResumeFn, loading, error };