mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-24 00:43:29 +10:00
v5.0.12 (#2814)
* refactor to @base-ui/react * fix all * fixes to accordion * more updates * switch to chat/completions api from openai * update version to v5.0.12
This commit is contained in:
+12
-9
@@ -11,6 +11,7 @@ import { CommandPalette } from "@/components/command-palette";
|
||||
import { BreakpointIndicator } from "@/components/layout/breakpoint-indicator";
|
||||
import { ThemeProvider } from "@/components/theme/provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { DialogManager } from "@/dialogs/manager";
|
||||
import { ConfirmDialogProvider } from "@/hooks/use-confirm";
|
||||
import { PromptDialogProvider } from "@/hooks/use-prompt";
|
||||
@@ -116,17 +117,19 @@ function RootDocument({ children }: Props) {
|
||||
<I18nProvider i18n={i18n}>
|
||||
<IconContext.Provider value={{ size: 16, weight: "regular" }}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ConfirmDialogProvider>
|
||||
<PromptDialogProvider>
|
||||
{children}
|
||||
<TooltipProvider>
|
||||
<ConfirmDialogProvider>
|
||||
<PromptDialogProvider>
|
||||
{children}
|
||||
|
||||
<DialogManager />
|
||||
<CommandPalette />
|
||||
<Toaster richColors position="bottom-right" />
|
||||
<DialogManager />
|
||||
<CommandPalette />
|
||||
<Toaster richColors position="bottom-right" />
|
||||
|
||||
{import.meta.env.DEV && <BreakpointIndicator />}
|
||||
</PromptDialogProvider>
|
||||
</ConfirmDialogProvider>
|
||||
{import.meta.env.DEV && <BreakpointIndicator />}
|
||||
</PromptDialogProvider>
|
||||
</ConfirmDialogProvider>
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</IconContext.Provider>
|
||||
</I18nProvider>
|
||||
|
||||
@@ -204,21 +204,31 @@ export const DonationBanner = () => (
|
||||
viewport={{ once: true }}
|
||||
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">
|
||||
<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>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
nativeButton={false}
|
||||
className="h-11 gap-2 px-6"
|
||||
render={
|
||||
<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>
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button asChild size="lg" className="h-11 gap-2 px-6">
|
||||
<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>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
nativeButton={false}
|
||||
className="h-11 gap-2 px-6"
|
||||
render={
|
||||
<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>
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Footer note */}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { CaretRightIcon } from "@phosphor-icons/react";
|
||||
import { motion } from "motion/react";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
@@ -92,7 +91,7 @@ export function FAQ() {
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
>
|
||||
<Accordion type="multiple">
|
||||
<Accordion multiple>
|
||||
{faqItems.map((item, index) => (
|
||||
<FAQItemComponent key={item.question} item={item} index={index} />
|
||||
))}
|
||||
@@ -117,10 +116,7 @@ function FAQItemComponent({ item, index }: FAQItemComponentProps) {
|
||||
transition={{ duration: 0.4, delay: index * 0.05 }}
|
||||
>
|
||||
<AccordionItem value={item.question} className="group border-t">
|
||||
<AccordionTrigger className="py-5">
|
||||
{item.question}
|
||||
<CaretRightIcon aria-hidden="true" className="shrink-0 transition-transform duration-200" />
|
||||
</AccordionTrigger>
|
||||
<AccordionTrigger className="py-5">{item.question}</AccordionTrigger>
|
||||
<AccordionContent className="pb-5 text-muted-foreground leading-relaxed">{item.answer}</AccordionContent>
|
||||
</AccordionItem>
|
||||
</motion.div>
|
||||
|
||||
@@ -72,16 +72,22 @@ export function Footer() {
|
||||
{/* Social Links */}
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
{socialLinks.map((social) => (
|
||||
<Button key={social.label} size="icon-sm" variant="ghost" asChild>
|
||||
<a
|
||||
href={social.url}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
aria-label={`${social.label} (${t`opens in new tab`})`}
|
||||
>
|
||||
<social.icon aria-hidden="true" size={18} />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
key={social.label}
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a
|
||||
href={social.url}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
aria-label={`${social.label} (${t`opens in new tab`})`}
|
||||
>
|
||||
<social.icon aria-hidden="true" size={18} />
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,13 +58,11 @@ export function Header() {
|
||||
|
||||
<div className="ml-auto flex items-center gap-x-2">
|
||||
<LocaleCombobox
|
||||
buttonProps={{
|
||||
size: "icon",
|
||||
variant: "ghost",
|
||||
className: "justify-center",
|
||||
"aria-label": t`Change language`,
|
||||
children: () => <TranslateIcon aria-hidden="true" />,
|
||||
}}
|
||||
render={
|
||||
<Button size="icon" variant="ghost">
|
||||
<TranslateIcon />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<ThemeToggleButton />
|
||||
@@ -72,11 +70,16 @@ export function Header() {
|
||||
<div className="hidden items-center gap-x-4 sm:flex">
|
||||
<GithubStarsButton />
|
||||
|
||||
<Button asChild size="icon" aria-label={t`Go to dashboard`}>
|
||||
<Link to="/dashboard">
|
||||
<ArrowRightIcon aria-hidden="true" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
nativeButton={false}
|
||||
aria-label={t`Go to dashboard`}
|
||||
render={
|
||||
<Link to="/dashboard">
|
||||
<ArrowRightIcon aria-hidden="true" />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -91,27 +91,38 @@ export function Hero() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 1.4 }}
|
||||
>
|
||||
<Button asChild size="lg" className="group relative overflow-hidden px-4">
|
||||
<Link to="/dashboard">
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
<Trans>Get Started</Trans>
|
||||
<ArrowRightIcon
|
||||
aria-hidden="true"
|
||||
className="size-4 transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
nativeButton={false}
|
||||
className="group relative overflow-hidden px-4"
|
||||
render={
|
||||
<Link to="/dashboard">
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
<Trans>Get Started</Trans>
|
||||
<ArrowRightIcon
|
||||
aria-hidden="true"
|
||||
className="size-4 transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</span>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button asChild size="lg" variant="ghost" className="gap-2 px-4">
|
||||
<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">
|
||||
<Trans>(opens in new tab)</Trans>
|
||||
</span>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="ghost"
|
||||
className="gap-2 px-4"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<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">
|
||||
<Trans>(opens in new tab)</Trans>
|
||||
</span>
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -64,11 +64,16 @@ function RouteComponent() {
|
||||
<div className="text-muted-foreground">
|
||||
<Trans>
|
||||
Remember your password?{" "}
|
||||
<Button asChild variant="link" className="h-auto gap-1.5 px-1! py-0">
|
||||
<Link to="/auth/login">
|
||||
Sign in now <ArrowRightIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
className="h-auto gap-1.5 px-1! py-0"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<Link to="/auth/login">
|
||||
Sign in now <ArrowRightIcon />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,9 +88,9 @@ function RouteComponent() {
|
||||
<FormLabel>
|
||||
<Trans>Email Address</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" autoComplete="email" placeholder="john.doe@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={<Input type="email" autoComplete="email" placeholder="john.doe@example.com" {...field} />}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -112,11 +117,14 @@ function PostForgotPasswordScreen() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button asChild>
|
||||
<a href="mailto:">
|
||||
<Trans>Open Email Client</Trans>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a href="mailto:">
|
||||
<Trans>Open Email Client</Trans>
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+42
-27
@@ -90,11 +90,16 @@ function RouteComponent() {
|
||||
<div className="text-muted-foreground">
|
||||
<Trans>
|
||||
Don't have an account?{" "}
|
||||
<Button asChild variant="link" className="h-auto gap-1.5 px-1! py-0">
|
||||
<Link to="/auth/register">
|
||||
Create one now <ArrowRightIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
nativeButton={false}
|
||||
className="h-auto gap-1.5 px-1! py-0"
|
||||
render={
|
||||
<Link to="/auth/register">
|
||||
Create one now <ArrowRightIcon />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
@@ -111,14 +116,16 @@ function RouteComponent() {
|
||||
<FormLabel>
|
||||
<Trans>Email Address</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoComplete="section-login username"
|
||||
placeholder="john.doe@example.com"
|
||||
className="lowercase"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Input
|
||||
autoComplete="section-login username"
|
||||
placeholder="john.doe@example.com"
|
||||
className="lowercase"
|
||||
{...field}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
<Trans>You can also use your username to login.</Trans>
|
||||
@@ -137,22 +144,30 @@ function RouteComponent() {
|
||||
<Trans>Password</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<Button asChild tabIndex={-1} variant="link" className="h-auto p-0 text-xs leading-none">
|
||||
<Link to="/auth/forgot-password">
|
||||
<Trans>Forgot Password?</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
variant="link"
|
||||
nativeButton={false}
|
||||
className="h-auto p-0 text-xs leading-none"
|
||||
render={
|
||||
<Link to="/auth/forgot-password">
|
||||
<Trans>Forgot Password?</Trans>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<FormControl>
|
||||
<Input
|
||||
min={6}
|
||||
max={64}
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="section-login current-password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Input
|
||||
min={6}
|
||||
max={64}
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="section-login current-password"
|
||||
{...field}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button size="icon" variant="ghost" onClick={toggleShowPassword}>
|
||||
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
|
||||
|
||||
@@ -89,11 +89,16 @@ function RouteComponent() {
|
||||
<div className="text-muted-foreground">
|
||||
<Trans>
|
||||
Already have an account?{" "}
|
||||
<Button asChild variant="link" className="h-auto gap-1.5 px-1! py-0">
|
||||
<Link to="/auth/login">
|
||||
Sign in now <ArrowRightIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
nativeButton={false}
|
||||
className="h-auto gap-1.5 px-1! py-0"
|
||||
render={
|
||||
<Link to="/auth/login">
|
||||
Sign in now <ArrowRightIcon />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,9 +114,11 @@ function RouteComponent() {
|
||||
<FormLabel>
|
||||
<Trans>Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input min={3} max={64} autoComplete="section-register name" placeholder="John Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Input min={3} max={64} autoComplete="section-register name" placeholder="John Doe" {...field} />
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -125,16 +132,18 @@ function RouteComponent() {
|
||||
<FormLabel>
|
||||
<Trans>Username</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
min={3}
|
||||
max={64}
|
||||
autoComplete="section-register username"
|
||||
placeholder="john.doe"
|
||||
className="lowercase"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Input
|
||||
min={3}
|
||||
max={64}
|
||||
autoComplete="section-register username"
|
||||
placeholder="john.doe"
|
||||
className="lowercase"
|
||||
{...field}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -148,15 +157,17 @@ function RouteComponent() {
|
||||
<FormLabel>
|
||||
<Trans>Email Address</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
autoComplete="section-register email"
|
||||
placeholder="john.doe@example.com"
|
||||
className="lowercase"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Input
|
||||
type="email"
|
||||
autoComplete="section-register email"
|
||||
placeholder="john.doe@example.com"
|
||||
className="lowercase"
|
||||
{...field}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -171,15 +182,17 @@ function RouteComponent() {
|
||||
<Trans>Password</Trans>
|
||||
</FormLabel>
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<FormControl>
|
||||
<Input
|
||||
min={6}
|
||||
max={64}
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="section-register new-password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Input
|
||||
min={6}
|
||||
max={64}
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="section-register new-password"
|
||||
{...field}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button size="icon" variant="ghost" onClick={toggleShowPassword}>
|
||||
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
|
||||
@@ -223,11 +236,14 @@ function PostSignupScreen() {
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button asChild>
|
||||
<Link to="/dashboard">
|
||||
<Trans>Continue</Trans> <ArrowRightIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
render={
|
||||
<Link to="/dashboard">
|
||||
<Trans>Continue</Trans> <ArrowRightIcon />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,15 +85,17 @@ function RouteComponent() {
|
||||
<Trans>New Password</Trans>
|
||||
</FormLabel>
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<FormControl>
|
||||
<Input
|
||||
min={6}
|
||||
max={64}
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Input
|
||||
min={6}
|
||||
max={64}
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
{...field}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button size="icon" variant="ghost" onClick={toggleShowPassword}>
|
||||
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
|
||||
|
||||
@@ -104,15 +104,17 @@ function RouteComponent() {
|
||||
<Trans>Password</Trans>
|
||||
</FormLabel>
|
||||
<div className="flex items-center gap-x-1.5">
|
||||
<FormControl>
|
||||
<Input
|
||||
min={6}
|
||||
max={64}
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Input
|
||||
min={6}
|
||||
max={64}
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
{...field}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button size="icon" variant="ghost" onClick={toggleShowPassword}>
|
||||
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
|
||||
|
||||
@@ -70,44 +70,52 @@ function RouteComponent() {
|
||||
name="code"
|
||||
render={({ field }) => (
|
||||
<FormItem className="justify-self-center">
|
||||
<FormControl>
|
||||
<InputOTP
|
||||
maxLength={10}
|
||||
value={field.value}
|
||||
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
||||
onChange={field.onChange}
|
||||
onComplete={form.handleSubmit(onSubmit)}
|
||||
pasteTransformer={(pasted) => pasted.replaceAll("-", "")}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} className="size-12" />
|
||||
<InputOTPSlot index={1} className="size-12" />
|
||||
<InputOTPSlot index={2} className="size-12" />
|
||||
<InputOTPSlot index={3} className="size-12" />
|
||||
<InputOTPSlot index={4} className="size-12" />
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={5} className="size-12" />
|
||||
<InputOTPSlot index={6} className="size-12" />
|
||||
<InputOTPSlot index={7} className="size-12" />
|
||||
<InputOTPSlot index={8} className="size-12" />
|
||||
<InputOTPSlot index={9} className="size-12" />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputOTP
|
||||
maxLength={10}
|
||||
value={field.value}
|
||||
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
||||
onChange={field.onChange}
|
||||
onComplete={form.handleSubmit(onSubmit)}
|
||||
pasteTransformer={(pasted) => pasted.replaceAll("-", "")}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} className="size-12" />
|
||||
<InputOTPSlot index={1} className="size-12" />
|
||||
<InputOTPSlot index={2} className="size-12" />
|
||||
<InputOTPSlot index={3} className="size-12" />
|
||||
<InputOTPSlot index={4} className="size-12" />
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={5} className="size-12" />
|
||||
<InputOTPSlot index={6} className="size-12" />
|
||||
<InputOTPSlot index={7} className="size-12" />
|
||||
<InputOTPSlot index={8} className="size-12" />
|
||||
<InputOTPSlot index={9} className="size-12" />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-x-2">
|
||||
<Button type="button" variant="outline" className="flex-1" asChild>
|
||||
<Link to="/auth/verify-2fa">
|
||||
<ArrowLeftIcon />
|
||||
<Trans>Go Back</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<Link to="/auth/verify-2fa">
|
||||
<ArrowLeftIcon />
|
||||
<Trans>Go Back</Trans>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="flex-1">
|
||||
<CheckIcon />
|
||||
<Trans>Verify</Trans>
|
||||
|
||||
@@ -71,40 +71,47 @@ function RouteComponent() {
|
||||
name="code"
|
||||
render={({ field }) => (
|
||||
<FormItem className="justify-self-center">
|
||||
<FormControl>
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={field.value}
|
||||
pattern={REGEXP_ONLY_DIGITS}
|
||||
onChange={field.onChange}
|
||||
onComplete={form.handleSubmit(onSubmit)}
|
||||
pasteTransformer={(pasted) => pasted.replaceAll("-", "")}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} className="size-12" />
|
||||
<InputOTPSlot index={1} className="size-12" />
|
||||
<InputOTPSlot index={2} className="size-12" />
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={3} className="size-12" />
|
||||
<InputOTPSlot index={4} className="size-12" />
|
||||
<InputOTPSlot index={5} className="size-12" />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={field.value}
|
||||
pattern={REGEXP_ONLY_DIGITS}
|
||||
onChange={field.onChange}
|
||||
onComplete={form.handleSubmit(onSubmit)}
|
||||
pasteTransformer={(pasted) => pasted.replaceAll("-", "")}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} className="size-12" />
|
||||
<InputOTPSlot index={1} className="size-12" />
|
||||
<InputOTPSlot index={2} className="size-12" />
|
||||
</InputOTPGroup>
|
||||
<InputOTPSeparator />
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={3} className="size-12" />
|
||||
<InputOTPSlot index={4} className="size-12" />
|
||||
<InputOTPSlot index={5} className="size-12" />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-x-2">
|
||||
<Button type="button" variant="outline" className="flex-1" asChild>
|
||||
<Link to="/auth/login">
|
||||
<ArrowLeftIcon />
|
||||
<Trans>Back to Login</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<Link to="/auth/login">
|
||||
<ArrowLeftIcon />
|
||||
<Trans>Back to Login</Trans>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="flex-1">
|
||||
<CheckIcon />
|
||||
@@ -112,11 +119,17 @@ function RouteComponent() {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<Button type="button" variant="link" className="h-auto justify-self-center p-0 text-sm" asChild>
|
||||
<Link to="/auth/verify-2fa-backup">
|
||||
<Trans>Lost access to your authenticator?</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="link"
|
||||
nativeButton={false}
|
||||
className="h-auto justify-self-center p-0 text-sm"
|
||||
render={
|
||||
<Link to="/auth/verify-2fa-backup">
|
||||
<Trans>Lost access to your authenticator?</Trans>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -148,11 +148,14 @@ type DockIconProps = {
|
||||
function DockIcon({ icon: Icon, title, disabled, onClick, iconClassName }: DockIconProps) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="icon" variant="ghost" disabled={disabled} onClick={onClick}>
|
||||
<Icon className={cn("size-4", iconClassName)} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button size="icon" variant="ghost" disabled={disabled} onClick={onClick}>
|
||||
<Icon className={cn("size-4", iconClassName)} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<TooltipContent side="top" align="center" className="font-medium">
|
||||
{title}
|
||||
</TooltipContent>
|
||||
|
||||
@@ -39,11 +39,16 @@ export function BuilderHeader() {
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-x-1">
|
||||
<Button asChild size="icon" variant="ghost">
|
||||
<Link to="/dashboard/resumes" search={{ sort: "lastUpdatedAt", tags: [] }}>
|
||||
<HouseSimpleIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<Link to="/dashboard/resumes" search={{ sort: "lastUpdatedAt", tags: [] }}>
|
||||
<HouseSimpleIcon />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<span className="me-2.5 text-muted-foreground">/</span>
|
||||
<h2 className="flex-1 truncate font-medium">{name}</h2>
|
||||
{isLocked && <LockSimpleIcon className="ms-2 text-muted-foreground" />}
|
||||
@@ -123,31 +128,33 @@ function BuilderHeaderDropdown() {
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon" variant="ghost">
|
||||
<CaretDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button size="icon" variant="ghost">
|
||||
<CaretDownIcon />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem disabled={isLocked} onSelect={handleUpdate}>
|
||||
<DropdownMenuItem disabled={isLocked} onClick={handleUpdate}>
|
||||
<PencilSimpleLineIcon className="me-2" />
|
||||
<Trans>Update</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onSelect={handleDuplicate}>
|
||||
<DropdownMenuItem onClick={handleDuplicate}>
|
||||
<CopySimpleIcon className="me-2" />
|
||||
<Trans>Duplicate</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onSelect={handleToggleLock}>
|
||||
<DropdownMenuItem onClick={handleToggleLock}>
|
||||
{isLocked ? <LockSimpleOpenIcon className="me-2" /> : <LockSimpleIcon className="me-2" />}
|
||||
{isLocked ? <Trans>Unlock</Trans> : <Trans>Lock</Trans>}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem variant="destructive" disabled={isLocked} onSelect={handleDelete}>
|
||||
<DropdownMenuItem variant="destructive" disabled={isLocked} onClick={handleDelete}>
|
||||
<TrashSimpleIcon className="me-2" />
|
||||
<Trans>Delete</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -49,9 +49,7 @@ function BasicsSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormControl render={<Input {...field} />} />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -65,9 +63,7 @@ function BasicsSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Headline</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormControl render={<Input {...field} />} />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -81,9 +77,7 @@ function BasicsSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Email</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormControl render={<Input type="email" {...field} />} />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -97,9 +91,7 @@ function BasicsSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Phone</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormControl render={<Input {...field} />} />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -113,9 +105,7 @@ function BasicsSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Location</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormControl render={<Input {...field} />} />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -129,9 +119,7 @@ function BasicsSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Website</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<URLInput {...field} value={field.value} onChange={field.onChange} />
|
||||
</FormControl>
|
||||
<URLInput {...field} value={field.value} onChange={field.onChange} />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IconPicker } from "@/components/input/icon-picker";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FormControl, FormField, FormItem } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import type { basicsSchema } from "@/schema/resume/data";
|
||||
import { generateId } from "@/utils/string";
|
||||
@@ -55,16 +56,18 @@ export function CustomFieldsSection({ onSubmit }: Props) {
|
||||
name={`customFields.${index}.icon`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="shrink-0">
|
||||
<FormControl>
|
||||
<IconPicker
|
||||
{...field}
|
||||
className="rounded-r-none! border-e-0!"
|
||||
onChange={(icon) => {
|
||||
field.onChange(icon);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<IconPicker
|
||||
{...field}
|
||||
className="rounded-r-none! border-e-0!"
|
||||
onChange={(icon) => {
|
||||
field.onChange(icon);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -74,32 +77,36 @@ export function CustomFieldsSection({ onSubmit }: Props) {
|
||||
name={`customFields.${index}.text`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="rounded-l-none!"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Input
|
||||
{...field}
|
||||
className="rounded-l-none!"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button size="icon" variant="ghost" className="ms-1">
|
||||
<LinkIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button size="icon" variant="ghost" className="ms-1">
|
||||
<LinkIcon />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<PopoverContent align="center">
|
||||
<div className="flex flex-col gap-y-1.5">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
<Label htmlFor={`customFields.${index}.link`} className="text-muted-foreground text-xs">
|
||||
<Trans>Enter the URL to link to</Trans>
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
@@ -108,6 +115,7 @@ export function CustomFieldsSection({ onSubmit }: Props) {
|
||||
<Input
|
||||
type="url"
|
||||
value={field.value}
|
||||
id={`customFields.${index}.link`}
|
||||
placeholder="Must start with https://"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
|
||||
@@ -232,28 +232,23 @@ function CustomSectionDropdownMenu({ section }: { section: CustomSection }) {
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-context-menu items-center px-1.5 opacity-40 transition-[background-color,opacity] hover:bg-secondary/40 focus:outline-none focus-visible:ring-1 group-hover:opacity-100"
|
||||
>
|
||||
<DotsThreeVerticalIcon />
|
||||
</button>
|
||||
<DropdownMenuTrigger>
|
||||
<DotsThreeVerticalIcon />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={onToggleSectionVisibility}>
|
||||
<DropdownMenuItem onClick={onToggleSectionVisibility}>
|
||||
{section.hidden ? <EyeIcon /> : <EyeClosedIcon />}
|
||||
{section.hidden ? <Trans>Show</Trans> : <Trans>Hide</Trans>}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onSelect={onUpdateSection}>
|
||||
<DropdownMenuItem onClick={onUpdateSection}>
|
||||
<PencilSimpleLineIcon />
|
||||
<Trans>Update</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onSelect={onDuplicateSection}>
|
||||
<DropdownMenuItem onClick={onDuplicateSection}>
|
||||
<CopySimpleIcon />
|
||||
<Trans>Duplicate</Trans>
|
||||
</DropdownMenuItem>
|
||||
@@ -279,7 +274,7 @@ function CustomSectionDropdownMenu({ section }: { section: CustomSection }) {
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive" onSelect={onDeleteSection}>
|
||||
<DropdownMenuItem variant="destructive" onClick={onDeleteSection}>
|
||||
<TrashSimpleIcon />
|
||||
<Trans>Delete</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -119,9 +119,7 @@ function PictureSectionForm() {
|
||||
<Trans>URL</Trans>
|
||||
</FormLabel>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormControl render={<Input {...field} />} />
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
@@ -180,20 +178,22 @@ function PictureSectionForm() {
|
||||
<Trans>Rotation</Trans>
|
||||
</FormLabel>
|
||||
<InputGroup>
|
||||
<FormControl>
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
max={360}
|
||||
step={5}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
max={360}
|
||||
step={5}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>°</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
@@ -211,20 +211,22 @@ function PictureSectionForm() {
|
||||
<Trans>Aspect Ratio</Trans>
|
||||
</FormLabel>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0.5}
|
||||
max={2.5}
|
||||
step={0.1}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0.5}
|
||||
max={2.5}
|
||||
step={0.1}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ButtonGroup className="shrink-0">
|
||||
<Button
|
||||
@@ -247,7 +249,7 @@ function PictureSectionForm() {
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
>
|
||||
<div className="aspect-[1.5/1] min-h-3 min-w-3 border border-primary" />
|
||||
<div className="aspect-1.5/1 min-h-3 min-w-3 border border-primary" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
@@ -258,7 +260,7 @@ function PictureSectionForm() {
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
>
|
||||
<div className="aspect-[1/1.5] min-h-3 min-w-3 border border-primary" />
|
||||
<div className="aspect-1/1.5 min-h-3 min-w-3 border border-primary" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
@@ -276,19 +278,21 @@ function PictureSectionForm() {
|
||||
</FormLabel>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<InputGroup>
|
||||
<FormControl>
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">pt</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
@@ -332,21 +336,23 @@ function PictureSectionForm() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="flex items-end gap-x-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="borderColor"
|
||||
render={({ field }) => (
|
||||
<FormItem className="shrink-0 self-end">
|
||||
<FormControl>
|
||||
<ColorPicker
|
||||
defaultValue={field.value}
|
||||
onValueChange={(color) => {
|
||||
field.onChange(color);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormItem className="mb-1.5 shrink-0">
|
||||
<FormControl
|
||||
render={
|
||||
<ColorPicker
|
||||
defaultValue={field.value}
|
||||
onChange={(color) => {
|
||||
field.onChange(color);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -360,19 +366,21 @@ function PictureSectionForm() {
|
||||
<Trans>Border Width</Trans>
|
||||
</FormLabel>
|
||||
<InputGroup>
|
||||
<FormControl>
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
step={1}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
step={1}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>pt</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
@@ -382,21 +390,23 @@ function PictureSectionForm() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="flex items-end gap-x-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="shadowColor"
|
||||
render={({ field }) => (
|
||||
<FormItem className="shrink-0 self-end">
|
||||
<FormControl>
|
||||
<ColorPicker
|
||||
defaultValue={field.value}
|
||||
onValueChange={(color) => {
|
||||
field.onChange(color);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormItem className="mb-1.5 shrink-0">
|
||||
<FormControl
|
||||
render={
|
||||
<ColorPicker
|
||||
defaultValue={field.value}
|
||||
onChange={(color) => {
|
||||
field.onChange(color);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -410,19 +420,21 @@ function PictureSectionForm() {
|
||||
<Trans>Shadow Width</Trans>
|
||||
</FormLabel>
|
||||
<InputGroup>
|
||||
<FormControl>
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.5}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.5}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>pt</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CaretRightIcon } from "@phosphor-icons/react";
|
||||
import { CaretDownIcon } from "@phosphor-icons/react";
|
||||
import { useResumeStore } from "@/components/resume/store/resume";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -27,20 +27,21 @@ export function SectionBase({ type, className, ...props }: Props) {
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
collapsible
|
||||
type="single"
|
||||
id={`sidebar-${type}`}
|
||||
value={collapsed ? "" : type}
|
||||
value={collapsed ? [] : [type]}
|
||||
onValueChange={() => toggleCollapsed(type)}
|
||||
className={cn("space-y-4", isHidden && "opacity-50")}
|
||||
>
|
||||
<AccordionItem value={type} className="group/accordion space-y-4">
|
||||
<AccordionItem value={type} className="group/accordion-item space-y-4">
|
||||
<div className="flex items-center">
|
||||
<AccordionTrigger asChild className="me-2 items-center justify-center">
|
||||
<Button size="icon" variant="ghost">
|
||||
<CaretRightIcon />
|
||||
</Button>
|
||||
</AccordionTrigger>
|
||||
<AccordionTrigger
|
||||
className="me-2 items-center justify-center"
|
||||
render={
|
||||
<Button size="icon" variant="ghost">
|
||||
<CaretDownIcon className="transition-transform duration-200 group-data-closed/accordion-item:-rotate-90" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 items-center gap-x-4">
|
||||
{getSectionIcon(type)}
|
||||
@@ -56,7 +57,7 @@ export function SectionBase({ type, className, ...props }: Props) {
|
||||
|
||||
<AccordionContent
|
||||
className={cn(
|
||||
"overflow-hidden pb-0 data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
|
||||
"p-0 data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -125,7 +125,7 @@ function MoveItemSubmenu({ type, item, customSectionId }: MoveItemSubmenuProps)
|
||||
<DropdownMenuSubContent>
|
||||
{/* Existing compatible sections on this page */}
|
||||
{sections.map(({ sectionId, sectionTitle }) => (
|
||||
<DropdownMenuItem key={sectionId} onSelect={() => handleMoveToSection(sectionId)}>
|
||||
<DropdownMenuItem key={sectionId} onClick={() => handleMoveToSection(sectionId)}>
|
||||
{sectionTitle}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
@@ -134,7 +134,7 @@ function MoveItemSubmenu({ type, item, customSectionId }: MoveItemSubmenuProps)
|
||||
{sections.length > 0 && <DropdownMenuSeparator />}
|
||||
|
||||
{/* Option to create a new section on this page */}
|
||||
<DropdownMenuItem onSelect={() => handleNewSectionOnPage(pageIndex)}>
|
||||
<DropdownMenuItem onClick={() => handleNewSectionOnPage(pageIndex)}>
|
||||
<FolderPlusIcon />
|
||||
<Trans>New Section</Trans>
|
||||
</DropdownMenuItem>
|
||||
@@ -145,7 +145,7 @@ function MoveItemSubmenu({ type, item, customSectionId }: MoveItemSubmenuProps)
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* Option to create a new page with a new section */}
|
||||
<DropdownMenuItem onSelect={handleNewPage}>
|
||||
<DropdownMenuItem onClick={handleNewPage}>
|
||||
<PlusCircleIcon />
|
||||
<Trans>New Page</Trans>
|
||||
</DropdownMenuItem>
|
||||
@@ -266,15 +266,13 @@ export function SectionItem<T extends CustomSectionItem | SectionItemType>({
|
||||
</button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="flex cursor-context-menu items-center px-1.5 opacity-40 transition-[background-color,opacity] hover:bg-secondary/40 focus:outline-none focus-visible:ring-1 group-hover:opacity-100">
|
||||
<DotsThreeVerticalIcon />
|
||||
</button>
|
||||
<DropdownMenuTrigger className="flex cursor-context-menu items-center px-1.5 opacity-40 transition-[background-color,opacity] hover:bg-secondary/40 focus:outline-none focus-visible:ring-1 group-hover:opacity-100">
|
||||
<DotsThreeVerticalIcon />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={onToggleVisibility}>
|
||||
<DropdownMenuItem onClick={onToggleVisibility}>
|
||||
{item.hidden ? <EyeIcon /> : <EyeClosedIcon />}
|
||||
{item.hidden ? <Trans>Show</Trans> : <Trans>Hide</Trans>}
|
||||
</DropdownMenuItem>
|
||||
@@ -283,12 +281,12 @@ export function SectionItem<T extends CustomSectionItem | SectionItemType>({
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={onUpdate}>
|
||||
<DropdownMenuItem onClick={onUpdate}>
|
||||
<PencilSimpleLineIcon />
|
||||
<Trans>Update</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onSelect={onDuplicate}>
|
||||
<DropdownMenuItem onClick={onDuplicate}>
|
||||
<CopySimpleIcon />
|
||||
<Trans>Duplicate</Trans>
|
||||
</DropdownMenuItem>
|
||||
@@ -299,7 +297,7 @@ export function SectionItem<T extends CustomSectionItem | SectionItemType>({
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive" onSelect={onDelete}>
|
||||
<DropdownMenuItem variant="destructive" onClick={onDelete}>
|
||||
<TrashSimpleIcon />
|
||||
<Trans>Delete</Trans>
|
||||
</DropdownMenuItem>
|
||||
@@ -328,7 +326,6 @@ export function SectionAddItemButton({ type, customSectionId, className, childre
|
||||
|
||||
return (
|
||||
<Button
|
||||
tapScale={1}
|
||||
variant="ghost"
|
||||
onClick={handleAdd}
|
||||
className={cn("h-12 w-full justify-start rounded-t-none", className)}
|
||||
|
||||
@@ -105,17 +105,19 @@ export function SectionDropdownMenu({ type }: Props) {
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="icon" variant="ghost">
|
||||
<ListIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button size="icon" variant="ghost">
|
||||
<ListIcon />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuContent>
|
||||
{type !== "summary" && (
|
||||
<>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={onAddItem}>
|
||||
<DropdownMenuItem onClick={onAddItem}>
|
||||
<PlusIcon />
|
||||
<Trans>Add a new item</Trans>
|
||||
</DropdownMenuItem>
|
||||
@@ -126,12 +128,12 @@ export function SectionDropdownMenu({ type }: Props) {
|
||||
)}
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onSelect={onToggleVisibility}>
|
||||
<DropdownMenuItem onClick={onToggleVisibility}>
|
||||
{section.hidden ? <EyeIcon /> : <EyeClosedIcon />}
|
||||
{section.hidden ? <Trans>Show</Trans> : <Trans>Hide</Trans>}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onSelect={onRenameSection}>
|
||||
<DropdownMenuItem onClick={onRenameSection}>
|
||||
<PencilSimpleLineIcon />
|
||||
<Trans>Rename</Trans>
|
||||
</DropdownMenuItem>
|
||||
@@ -157,7 +159,7 @@ export function SectionDropdownMenu({ type }: Props) {
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive" onSelect={onReset}>
|
||||
<DropdownMenuItem variant="destructive" onClick={onReset}>
|
||||
<BroomIcon />
|
||||
<Trans>Reset</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -52,15 +52,17 @@ function CSSSectionForm() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-4">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Trans context="Turn On/Apply Custom CSS">Enable</Trans>
|
||||
</FormLabel>
|
||||
@@ -74,18 +76,20 @@ function CSSSectionForm() {
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormItem className="h-48 overflow-hidden rounded-md">
|
||||
<FormControl>
|
||||
<Suspense fallback={<Skeleton className="h-48 w-full" />}>
|
||||
<CSSMonacoEditor
|
||||
theme={theme}
|
||||
defaultValue={field.value}
|
||||
onChange={(value) => {
|
||||
field.onChange(value ?? "");
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Suspense fallback={<Skeleton className="h-48 w-full" />}>
|
||||
<CSSMonacoEditor
|
||||
theme={theme}
|
||||
defaultValue={field.value}
|
||||
onChange={(value) => {
|
||||
field.onChange(value ?? "");
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -72,17 +72,15 @@ function ColorSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Primary Color</Trans>
|
||||
</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<ColorPicker
|
||||
value={field.value}
|
||||
onValueChange={(color) => {
|
||||
onChange={(color) => {
|
||||
field.onChange(color);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormControl render={<Input {...field} />} />
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -97,17 +95,15 @@ function ColorSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Text Color</Trans>
|
||||
</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<ColorPicker
|
||||
defaultValue={field.value}
|
||||
onValueChange={(color) => {
|
||||
onChange={(color) => {
|
||||
field.onChange(color);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormControl render={<Input {...field} />} />
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -122,17 +118,15 @@ function ColorSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Background Color</Trans>
|
||||
</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<ColorPicker
|
||||
defaultValue={field.value}
|
||||
onValueChange={(color) => {
|
||||
onChange={(color) => {
|
||||
field.onChange(color);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormControl render={<Input {...field} />} />
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -240,7 +234,7 @@ function LevelSectionForm() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="icon"
|
||||
@@ -249,16 +243,18 @@ function LevelSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Icon</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<IconPicker
|
||||
size="default"
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<IconPicker
|
||||
size="icon"
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -271,16 +267,18 @@ function LevelSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Type</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<LevelTypeCombobox
|
||||
value={field.value}
|
||||
onValueChange={(value) => {
|
||||
if (!value) return;
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<LevelTypeCombobox
|
||||
value={field.value}
|
||||
onValueChange={(value) => {
|
||||
if (!value) return;
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -24,46 +24,82 @@ export function InformationSectionBuilder() {
|
||||
</Trans>
|
||||
</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">
|
||||
<HandHeartIcon />
|
||||
<span className="truncate">
|
||||
<Trans>Donate to Reactive Resume</Trans>
|
||||
</span>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
nativeButton={false}
|
||||
className="mt-2 whitespace-normal px-4! text-xs"
|
||||
render={
|
||||
<a href="http://opencollective.com/reactive-resume" target="_blank" rel="noopener">
|
||||
<HandHeartIcon />
|
||||
<span className="truncate">
|
||||
<Trans>Donate to Reactive Resume</Trans>
|
||||
</span>
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Trans>Documentation</Trans>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
className="text-xs"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a href="https://docs.rxresu.me" target="_blank" rel="noopener">
|
||||
<Trans>Documentation</Trans>
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button asChild size="sm" variant="link" className="text-xs">
|
||||
<a href="https://github.com/amruthpillai/reactive-resume" target="_blank" rel="noopener">
|
||||
<Trans>Source Code</Trans>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
className="text-xs"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a href="https://github.com/amruthpillai/reactive-resume" target="_blank" rel="noopener">
|
||||
<Trans>Source Code</Trans>
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button asChild size="sm" variant="link" className="text-xs">
|
||||
<a href="https://github.com/amruthpillai/reactive-resume/issues" target="_blank" rel="noopener">
|
||||
<Trans>Report a Bug</Trans>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
className="text-xs"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a href="https://github.com/amruthpillai/reactive-resume/issues" target="_blank" rel="noopener">
|
||||
<Trans>Report a Bug</Trans>
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button asChild size="sm" variant="link" className="text-xs">
|
||||
<a href="https://crowdin.com/project/reactive-resume" target="_blank" rel="noopener">
|
||||
<Trans>Translations</Trans>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
className="text-xs"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a href="https://crowdin.com/project/reactive-resume" target="_blank" rel="noopener">
|
||||
<Trans>Translations</Trans>
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button asChild size="sm" variant="link" className="text-xs">
|
||||
<a href="https://opencollective.com/reactive-resume" target="_blank" rel="noopener">
|
||||
<Trans>Sponsors</Trans>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
className="text-xs"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a href="https://opencollective.com/reactive-resume" target="_blank" rel="noopener">
|
||||
<Trans>Sponsors</Trans>
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SectionBase>
|
||||
);
|
||||
|
||||
@@ -53,35 +53,39 @@ function LayoutSectionForm() {
|
||||
<Trans>Sidebar Width</Trans>
|
||||
</FormLabel>
|
||||
<div className="flex items-center gap-4">
|
||||
<FormControl>
|
||||
<Slider
|
||||
min={10}
|
||||
max={50}
|
||||
step={0.01}
|
||||
value={[field.value]}
|
||||
onValueChange={(value) => field.onChange(value[0])}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<InputGroup className="w-auto shrink-0">
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
type="number"
|
||||
<FormControl
|
||||
render={
|
||||
<Slider
|
||||
min={10}
|
||||
max={50}
|
||||
step={0.1}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
step={0.01}
|
||||
value={[field.value]}
|
||||
onValueChange={(value) => field.onChange(Array.isArray(value) ? value[0] : value)}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>%</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
}
|
||||
/>
|
||||
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroup className="w-auto shrink-0">
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
type="number"
|
||||
min={10}
|
||||
max={50}
|
||||
step={0.1}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>%</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -51,16 +51,18 @@ function PageSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Language</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
options={getLocaleOptions()}
|
||||
value={field.value}
|
||||
onValueChange={(locale) => {
|
||||
field.onChange(locale);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Combobox
|
||||
options={getLocaleOptions()}
|
||||
value={field.value}
|
||||
onValueChange={(locale) => {
|
||||
field.onChange(locale);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -74,20 +76,22 @@ function PageSectionForm() {
|
||||
<FormLabel>
|
||||
<Trans context="Page Format (A4, Letter or Free-Form)">Format</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
options={[
|
||||
{ value: "a4", label: t`A4` },
|
||||
{ value: "letter", label: t`Letter` },
|
||||
{ value: "free-form", label: t`Free-Form` },
|
||||
]}
|
||||
value={field.value}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Combobox
|
||||
options={[
|
||||
{ value: "a4", label: t`A4` },
|
||||
{ value: "letter", label: t`Letter` },
|
||||
{ value: "free-form", label: t`Free-Form` },
|
||||
]}
|
||||
value={field.value}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -102,20 +106,22 @@ function PageSectionForm() {
|
||||
<Trans>Margin (Horizontal)</Trans>
|
||||
</FormLabel>
|
||||
<InputGroup>
|
||||
<FormControl>
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>pt</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
@@ -134,20 +140,22 @@ function PageSectionForm() {
|
||||
<Trans>Margin (Vertical)</Trans>
|
||||
</FormLabel>
|
||||
<InputGroup>
|
||||
<FormControl>
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>pt</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
@@ -166,19 +174,21 @@ function PageSectionForm() {
|
||||
<Trans>Spacing (Horizontal)</Trans>
|
||||
</FormLabel>
|
||||
<InputGroup>
|
||||
<FormControl>
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={0}
|
||||
step={1}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={0}
|
||||
step={1}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>pt</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
@@ -197,19 +207,21 @@ function PageSectionForm() {
|
||||
<Trans>Spacing (Vertical)</Trans>
|
||||
</FormLabel>
|
||||
<InputGroup>
|
||||
<FormControl>
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={0}
|
||||
step={1}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={0}
|
||||
step={1}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>pt</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
@@ -224,15 +236,17 @@ function PageSectionForm() {
|
||||
name="hideIcons"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-full flex items-center gap-x-3 py-2">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(checked);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormLabel>
|
||||
<Trans>Hide all icons on the resume</Trans>
|
||||
</FormLabel>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function StatisticsSectionBuilder() {
|
||||
|
||||
return (
|
||||
<SectionBase type="statistics">
|
||||
<Accordion collapsible type="single" value={statistics.isPublic ? "isPublic" : "isPrivate"}>
|
||||
<Accordion value={statistics.isPublic ? ["isPublic"] : ["isPrivate"]}>
|
||||
<AccordionItem value="isPrivate">
|
||||
<AccordionContent className="pb-0">
|
||||
<Alert>
|
||||
|
||||
@@ -47,9 +47,9 @@ function TypographySectionForm() {
|
||||
<form onChange={form.handleSubmit(onSubmit)} className="grid @md:grid-cols-2 grid-cols-1 gap-4">
|
||||
<div className="col-span-full flex items-center gap-x-2">
|
||||
<Separator className="basis-[16px]" />
|
||||
<p className="shrink-0 font-medium text-base">
|
||||
<div className="shrink-0 font-medium text-base leading-none">
|
||||
<Trans context="Body Text (paragraphs, lists, etc.)">Body</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="flex-1" />
|
||||
</div>
|
||||
|
||||
@@ -61,21 +61,23 @@ function TypographySectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Font Family</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<FontFamilyCombobox
|
||||
value={field.value}
|
||||
buttonProps={{ size: "lg", className: "text-base" }}
|
||||
onValueChange={(value) => {
|
||||
if (value === null) return;
|
||||
field.onChange(value);
|
||||
const nextWeights = getNextWeights(value);
|
||||
if (nextWeights !== null) {
|
||||
form.setValue("body.fontWeights", nextWeights, { shouldDirty: true });
|
||||
}
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<FontFamilyCombobox
|
||||
value={field.value}
|
||||
className="text-base"
|
||||
onValueChange={(value) => {
|
||||
if (value === null) return;
|
||||
field.onChange(value);
|
||||
const nextWeights = getNextWeights(value);
|
||||
if (nextWeights !== null) {
|
||||
form.setValue("body.fontWeights", nextWeights, { shouldDirty: true });
|
||||
}
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -89,16 +91,18 @@ function TypographySectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Font Weights</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<FontWeightCombobox
|
||||
value={field.value}
|
||||
fontFamily={bodyFontFamily}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<FontWeightCombobox
|
||||
value={field.value}
|
||||
fontFamily={bodyFontFamily}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -113,20 +117,22 @@ function TypographySectionForm() {
|
||||
<Trans>Font Size</Trans>
|
||||
</FormLabel>
|
||||
<InputGroup>
|
||||
<FormControl>
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={6}
|
||||
max={24}
|
||||
step={0.1}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={6}
|
||||
max={24}
|
||||
step={0.1}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>pt</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
@@ -144,20 +150,22 @@ function TypographySectionForm() {
|
||||
<Trans>Line Height</Trans>
|
||||
</FormLabel>
|
||||
<InputGroup>
|
||||
<FormControl>
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={0.5}
|
||||
max={4}
|
||||
step={0.05}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={0.5}
|
||||
max={4}
|
||||
step={0.05}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>x</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
@@ -168,9 +176,9 @@ function TypographySectionForm() {
|
||||
|
||||
<div className="col-span-full flex items-center gap-x-2">
|
||||
<Separator className="basis-[16px]" />
|
||||
<p className="shrink-0 font-medium text-base">
|
||||
<div className="shrink-0 font-medium text-base leading-none">
|
||||
<Trans context="Headings or Titles (H1, H2, H3, H4, H5, H6)">Heading</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="flex-1" />
|
||||
</div>
|
||||
|
||||
@@ -182,21 +190,23 @@ function TypographySectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Font Family</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<FontFamilyCombobox
|
||||
value={field.value}
|
||||
buttonProps={{ size: "lg", className: "text-base" }}
|
||||
onValueChange={(value) => {
|
||||
if (value === null) return;
|
||||
field.onChange(value);
|
||||
const nextWeights = getNextWeights(value);
|
||||
if (nextWeights !== null) {
|
||||
form.setValue("heading.fontWeights", nextWeights, { shouldDirty: true });
|
||||
}
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<FontFamilyCombobox
|
||||
value={field.value}
|
||||
className="text-base"
|
||||
onValueChange={(value) => {
|
||||
if (value === null) return;
|
||||
field.onChange(value);
|
||||
const nextWeights = getNextWeights(value);
|
||||
if (nextWeights !== null) {
|
||||
form.setValue("heading.fontWeights", nextWeights, { shouldDirty: true });
|
||||
}
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -210,16 +220,18 @@ function TypographySectionForm() {
|
||||
<FormLabel>
|
||||
<Trans>Font Weight</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<FontWeightCombobox
|
||||
value={field.value}
|
||||
fontFamily={headingFontFamily}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<FontWeightCombobox
|
||||
value={field.value}
|
||||
fontFamily={headingFontFamily}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -234,20 +246,22 @@ function TypographySectionForm() {
|
||||
<Trans>Font Size</Trans>
|
||||
</FormLabel>
|
||||
<InputGroup>
|
||||
<FormControl>
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={6}
|
||||
max={24}
|
||||
step={0.1}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={6}
|
||||
max={24}
|
||||
step={0.1}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>pt</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
@@ -265,20 +279,22 @@ function TypographySectionForm() {
|
||||
<Trans>Line Height</Trans>
|
||||
</FormLabel>
|
||||
<InputGroup>
|
||||
<FormControl>
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={0.5}
|
||||
max={4}
|
||||
step={0.05}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<InputGroupInput
|
||||
{...field}
|
||||
min={0.5}
|
||||
max={4}
|
||||
step={0.05}
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") field.onChange("");
|
||||
else field.onChange(Number(value));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>x</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CaretRightIcon } from "@phosphor-icons/react";
|
||||
import { CaretDownIcon } from "@phosphor-icons/react";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getSectionIcon, getSectionTitle, type RightSidebarSection } from "@/utils/resume/section";
|
||||
@@ -15,20 +15,21 @@ export function SectionBase({ type, className, ...props }: Props) {
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
collapsible
|
||||
type="single"
|
||||
className="space-y-4"
|
||||
id={`sidebar-${type}`}
|
||||
value={collapsed ? "" : type}
|
||||
value={collapsed ? [] : [type]}
|
||||
onValueChange={() => toggleCollapsed(type)}
|
||||
>
|
||||
<AccordionItem value={type} className="group/accordion space-y-4">
|
||||
<AccordionItem value={type} className="group/accordion-item space-y-4">
|
||||
<div className="flex items-center">
|
||||
<AccordionTrigger asChild className="me-2 items-center justify-center">
|
||||
<Button size="icon" variant="ghost">
|
||||
<CaretRightIcon />
|
||||
</Button>
|
||||
</AccordionTrigger>
|
||||
<AccordionTrigger
|
||||
className="me-2 items-center justify-center"
|
||||
render={
|
||||
<Button size="icon" variant="ghost">
|
||||
<CaretDownIcon className="transition-transform duration-200 group-data-closed/accordion-item:-rotate-90" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 items-center gap-x-4">
|
||||
{getSectionIcon(type)}
|
||||
|
||||
@@ -92,14 +92,17 @@ function SidebarItemList({ items }: SidebarItemListProps) {
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.href}>
|
||||
<SidebarMenuButton asChild title={i18n.t(item.label)}>
|
||||
<Link to={item.href} activeProps={{ className: "bg-sidebar-accent" }}>
|
||||
{item.icon}
|
||||
<span className="shrink-0 transition-[margin,opacity] duration-200 ease-in-out group-data-[collapsible=icon]:-ms-8 group-data-[collapsible=icon]:opacity-0">
|
||||
{i18n.t(item.label)}
|
||||
</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuButton
|
||||
title={i18n.t(item.label)}
|
||||
render={
|
||||
<Link to={item.href} activeProps={{ className: "bg-sidebar-accent" }}>
|
||||
{item.icon}
|
||||
<span className="shrink-0 transition-[margin,opacity] duration-200 ease-in-out group-data-[collapsible=icon]:-ms-8 group-data-[collapsible=icon]:opacity-0">
|
||||
{i18n.t(item.label)}
|
||||
</span>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
@@ -114,12 +117,15 @@ export function DashboardSidebar() {
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild className="h-auto justify-center">
|
||||
<Link to="/">
|
||||
<BrandIcon variant="icon" className="size-6" />
|
||||
<h1 className="sr-only">Reactive Resume</h1>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuButton
|
||||
className="h-auto justify-center"
|
||||
render={
|
||||
<Link to="/">
|
||||
<BrandIcon variant="icon" className="size-6" />
|
||||
<h1 className="sr-only">Reactive Resume</h1>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
|
||||
@@ -32,7 +32,6 @@ export function ListView({ resumes }: Props) {
|
||||
<Button
|
||||
size="lg"
|
||||
variant="ghost"
|
||||
tapScale={0.99}
|
||||
className="h-12 w-full justify-start gap-x-4 text-start"
|
||||
onClick={handleCreateResume}
|
||||
>
|
||||
@@ -56,7 +55,6 @@ export function ListView({ resumes }: Props) {
|
||||
<Button
|
||||
size="lg"
|
||||
variant="ghost"
|
||||
tapScale={0.99}
|
||||
className="h-12 w-full justify-start gap-x-4 text-start"
|
||||
onClick={handleImportResume}
|
||||
>
|
||||
@@ -100,21 +98,21 @@ function ResumeListItem({ resume }: { resume: Resume }) {
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
variant="ghost"
|
||||
tapScale={0.99}
|
||||
nativeButton={false}
|
||||
className="h-12 w-full flex-1 justify-start gap-x-4 text-start"
|
||||
>
|
||||
<Link to="/builder/$resumeId" params={{ resumeId: resume.id }}>
|
||||
<div className="size-3" />
|
||||
<div className="min-w-80 truncate">{resume.name}</div>
|
||||
render={
|
||||
<Link to="/builder/$resumeId" params={{ resumeId: resume.id }}>
|
||||
<div className="size-3" />
|
||||
<div className="min-w-80 truncate">{resume.name}</div>
|
||||
|
||||
<p className="text-xs opacity-60">
|
||||
<Trans>Last updated on {updatedAt}</Trans>
|
||||
</p>
|
||||
</Link>
|
||||
</Button>
|
||||
<p className="text-xs opacity-60">
|
||||
<Trans>Last updated on {updatedAt}</Trans>
|
||||
</p>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<ResumeDropdownMenu resume={resume} align="end">
|
||||
<Button size="icon" variant="ghost" className="size-12">
|
||||
|
||||
@@ -24,7 +24,7 @@ import { orpc, type RouterOutput } from "@/integrations/orpc/client";
|
||||
|
||||
type Props = {
|
||||
resume: RouterOutput["resume"]["list"][number];
|
||||
children: React.ReactNode;
|
||||
children: React.ComponentProps<typeof ContextMenuTrigger>["render"];
|
||||
};
|
||||
|
||||
export function ResumeContextMenu({ resume, children }: Props) {
|
||||
@@ -85,36 +85,38 @@ export function ResumeContextMenu({ resume, children }: Props) {
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||||
<ContextMenuTrigger render={children} />
|
||||
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem asChild>
|
||||
<Link to="/builder/$resumeId" params={{ resumeId: resume.id }}>
|
||||
<FolderOpenIcon />
|
||||
<Trans>Open</Trans>
|
||||
</Link>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
render={
|
||||
<Link to="/builder/$resumeId" params={{ resumeId: resume.id }}>
|
||||
<FolderOpenIcon />
|
||||
<Trans>Open</Trans>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<ContextMenuSeparator />
|
||||
|
||||
<ContextMenuItem disabled={resume.isLocked} onSelect={handleUpdate}>
|
||||
<ContextMenuItem disabled={resume.isLocked} onClick={handleUpdate}>
|
||||
<PencilSimpleLineIcon />
|
||||
<Trans>Update</Trans>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem onSelect={handleDuplicate}>
|
||||
<ContextMenuItem onClick={handleDuplicate}>
|
||||
<CopySimpleIcon />
|
||||
<Trans>Duplicate</Trans>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem onSelect={handleToggleLock}>
|
||||
<ContextMenuItem onClick={handleToggleLock}>
|
||||
{resume.isLocked ? <LockSimpleOpenIcon /> : <LockSimpleIcon />}
|
||||
{resume.isLocked ? <Trans>Unlock</Trans> : <Trans>Lock</Trans>}
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuSeparator />
|
||||
|
||||
<ContextMenuItem variant="destructive" disabled={resume.isLocked} onSelect={handleDelete}>
|
||||
<ContextMenuItem variant="destructive" disabled={resume.isLocked} onClick={handleDelete}>
|
||||
<TrashSimpleIcon />
|
||||
<Trans>Delete</Trans>
|
||||
</ContextMenuItem>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { orpc, type RouterOutput } from "@/integrations/orpc/client";
|
||||
|
||||
type Props = Omit<React.ComponentProps<typeof DropdownMenuContent>, "children"> & {
|
||||
resume: RouterOutput["resume"]["list"][number];
|
||||
children: React.ReactNode;
|
||||
children: React.ComponentProps<typeof DropdownMenuTrigger>["render"];
|
||||
};
|
||||
|
||||
export function ResumeDropdownMenu({ resume, children, ...props }: Props) {
|
||||
@@ -85,7 +85,7 @@ export function ResumeDropdownMenu({ resume, children, ...props }: Props) {
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger render={children} />
|
||||
|
||||
<DropdownMenuContent {...props}>
|
||||
<Link to="/builder/$resumeId" params={{ resumeId: resume.id }}>
|
||||
@@ -97,24 +97,24 @@ export function ResumeDropdownMenu({ resume, children, ...props }: Props) {
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem disabled={resume.isLocked} onSelect={handleUpdate}>
|
||||
<DropdownMenuItem disabled={resume.isLocked} onClick={handleUpdate}>
|
||||
<PencilSimpleLineIcon />
|
||||
<Trans>Update</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onSelect={handleDuplicate}>
|
||||
<DropdownMenuItem onClick={handleDuplicate}>
|
||||
<CopySimpleIcon />
|
||||
<Trans>Duplicate</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onSelect={handleToggleLock}>
|
||||
<DropdownMenuItem onClick={handleToggleLock}>
|
||||
{resume.isLocked ? <LockSimpleOpenIcon /> : <LockSimpleIcon />}
|
||||
{resume.isLocked ? <Trans>Unlock</Trans> : <Trans>Lock</Trans>}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem variant="destructive" disabled={resume.isLocked} onSelect={handleDelete}>
|
||||
<DropdownMenuItem variant="destructive" disabled={resume.isLocked} onClick={handleDelete}>
|
||||
<TrashSimpleIcon />
|
||||
<Trans>Delete</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { useLingui } from "@lingui/react";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { GridFourIcon, ListIcon, ReadCvLogoIcon, SortAscendingIcon, TagIcon } from "@phosphor-icons/react";
|
||||
import { GridFourIcon, ListIcon, ReadCvLogoIcon } from "@phosphor-icons/react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute, stripSearchParams, useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
@@ -9,9 +9,7 @@ import { getCookie, setCookie } from "@tanstack/react-start/server";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { useMemo } from "react";
|
||||
import z from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Combobox } from "@/components/ui/combobox";
|
||||
import { MultipleCombobox } from "@/components/ui/multiple-combobox";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { orpc } from "@/integrations/orpc/client";
|
||||
@@ -77,42 +75,21 @@ function RouteComponent() {
|
||||
<Combobox
|
||||
value={sort}
|
||||
options={sortOptions}
|
||||
placeholder={t`Sort by`}
|
||||
onValueChange={(value) => {
|
||||
if (!value) return;
|
||||
navigate({ search: { tags, sort: value as SortOption } });
|
||||
}}
|
||||
buttonProps={{
|
||||
title: t`Sort by`,
|
||||
variant: "ghost",
|
||||
children: (_, option) => (
|
||||
<>
|
||||
<SortAscendingIcon />
|
||||
{option?.label}
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<MultipleCombobox
|
||||
<Combobox
|
||||
multiple
|
||||
value={tags}
|
||||
options={tagOptions}
|
||||
placeholder={t`Filter by`}
|
||||
className={cn({ hidden: tagOptions.length === 0 })}
|
||||
onValueChange={(value) => {
|
||||
navigate({ search: { tags: value, sort } });
|
||||
}}
|
||||
buttonProps={{
|
||||
variant: "ghost",
|
||||
title: t`Filter by`,
|
||||
className: cn({ hidden: tagOptions.length === 0 }),
|
||||
children: (_, options) => (
|
||||
<>
|
||||
<TagIcon />
|
||||
{options.map((option) => (
|
||||
<Badge key={option.value} variant="outline">
|
||||
{option.label}
|
||||
</Badge>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
navigate({ search: { tags: value ?? [], sort } });
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -84,12 +84,16 @@ function RouteComponent() {
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button asChild variant="link">
|
||||
<a href="https://docs.rxresu.me/api-reference" target="_blank" rel="noopener">
|
||||
<LinkSimpleIcon />
|
||||
<Trans>API Reference</Trans>
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a href="https://docs.rxresu.me/api-reference" target="_blank" rel="noopener">
|
||||
<LinkSimpleIcon />
|
||||
<Trans>API Reference</Trans>
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -43,11 +43,15 @@ export function PasswordSection() {
|
||||
</Button>
|
||||
))
|
||||
.with(false, () => (
|
||||
<Button variant="outline" asChild>
|
||||
<Link to="/auth/forgot-password">
|
||||
<Trans>Set Password</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<Link to="/auth/forgot-password">
|
||||
<Trans>Set Password</Trans>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
))
|
||||
.exhaustive()}
|
||||
</motion.div>
|
||||
|
||||
@@ -40,16 +40,17 @@ function RouteComponent() {
|
||||
</Label>
|
||||
<LocaleCombobox />
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant="link"
|
||||
nativeButton={false}
|
||||
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">
|
||||
<Trans>Help translate the app to your language</Trans>
|
||||
<ArrowRightIcon className="size-3" />
|
||||
</a>
|
||||
</Button>
|
||||
render={
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -133,9 +133,9 @@ function RouteComponent() {
|
||||
<FormLabel>
|
||||
<Trans>Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input min={3} max={64} autoComplete="name" placeholder="John Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={<Input min={3} max={64} autoComplete="name" placeholder="John Doe" {...field} />}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -149,16 +149,18 @@ function RouteComponent() {
|
||||
<FormLabel>
|
||||
<Trans>Username</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
min={3}
|
||||
max={64}
|
||||
autoComplete="username"
|
||||
placeholder="john.doe"
|
||||
className="lowercase"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Input
|
||||
min={3}
|
||||
max={64}
|
||||
autoComplete="username"
|
||||
placeholder="john.doe"
|
||||
className="lowercase"
|
||||
{...field}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -172,15 +174,17 @@ function RouteComponent() {
|
||||
<FormLabel>
|
||||
<Trans>Email Address</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder="john.doe@example.com"
|
||||
className="lowercase"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
render={
|
||||
<Input
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder="john.doe@example.com"
|
||||
className="lowercase"
|
||||
{...field}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormMessage />
|
||||
{match(session.user.emailVerified)
|
||||
.with(true, () => (
|
||||
|
||||
Reference in New Issue
Block a user