mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-13 16:22:59 +10:00
design nosepass template, add tests, add template previews
This commit is contained in:
@ -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
|
||||
];
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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>
|
||||
@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user