mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-10 04:22:27 +10:00
feat(i18n): implement localization using LinguiJS
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@ -38,9 +38,14 @@ Thumbs.db
|
||||
|
||||
# Generated Files
|
||||
.nx
|
||||
.swc
|
||||
stats.html
|
||||
libs/prisma
|
||||
|
||||
# Environment Variables
|
||||
*.env*
|
||||
!.env.example
|
||||
!.env.example
|
||||
|
||||
# Lingui Compiled Messages
|
||||
apps/client/src/locales/_build/
|
||||
apps/client/src/locales/**/*.js
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -9,5 +9,8 @@
|
||||
"https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json": [
|
||||
"tools/compose/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"i18n-ally.localesPaths": [
|
||||
"apps/client/src/locales"
|
||||
]
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||

|
||||
[](https://hub.docker.com/repository/docker/amruthpillai/reactive-resume)
|
||||
[](https://github.com/sponsors/AmruthPillai)
|
||||
[](https://crowdin.com/project/reactive-resume)
|
||||
|
||||
# Reactive Resume
|
||||
|
||||
|
||||
@ -14,9 +14,29 @@
|
||||
"config": "tailwind.config.js"
|
||||
}
|
||||
},
|
||||
"plugins": ["lingui"],
|
||||
"rules": {
|
||||
// react-hooks
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
|
||||
// lingui
|
||||
"lingui/no-unlocalized-strings": 2,
|
||||
"lingui/t-call-in-function": 2,
|
||||
"lingui/no-single-variables-to-translate": 2,
|
||||
"lingui/no-expression-in-message": 2,
|
||||
"lingui/no-single-tag-to-translate": 2,
|
||||
"lingui/no-trans-inside-trans": 2,
|
||||
"lingui/text-restrictions": [
|
||||
2,
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"patterns": ["''", "\"", "’", "“"],
|
||||
"message": "This string contains forbidden characters"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
try {
|
||||
if (
|
||||
localStorage.theme === "dark" ||
|
||||
// eslint-disable-next-line lingui/no-unlocalized-strings
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
) {
|
||||
document.documentElement.classList.add("dark");
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import {
|
||||
CaretDown,
|
||||
ChatTeardropText,
|
||||
@ -39,15 +40,14 @@ export const AiActions = ({ value, onChange, className }: Props) => {
|
||||
|
||||
const onClick = async (action: Action, mood?: Mood) => {
|
||||
setLoading(action);
|
||||
let result = value;
|
||||
|
||||
// await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
let result = value;
|
||||
|
||||
if (action === "improve") result = await improveWriting(value);
|
||||
if (action === "fix") result = await fixGrammar(value);
|
||||
if (action === "tone" && mood) result = await changeTone(value, mood);
|
||||
|
||||
onChange("Result" + result);
|
||||
onChange(result);
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
@ -67,52 +67,52 @@ export const AiActions = ({ value, onChange, className }: Props) => {
|
||||
className="-rotate-90 bg-background px-2 text-[10px] leading-[10px]"
|
||||
>
|
||||
<MagicWand size={10} className="mr-1" />
|
||||
AI
|
||||
{t`AI`}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Button size="sm" variant="outline" disabled={!!loading} onClick={() => onClick("improve")}>
|
||||
{loading === "improve" ? <CircleNotch className="animate-spin" /> : <PenNib />}
|
||||
<span className="ml-2 text-xs">Improve Writing</span>
|
||||
<span className="ml-2 text-xs">{t`Improve Writing`}</span>
|
||||
</Button>
|
||||
|
||||
<Button size="sm" variant="outline" disabled={!!loading} onClick={() => onClick("fix")}>
|
||||
{loading === "fix" ? <CircleNotch className="animate-spin" /> : <Exam />}
|
||||
<span className="ml-2 text-xs">Fix Spelling & Grammar</span>
|
||||
<span className="ml-2 text-xs">{t`Fix Spelling & Grammar`}</span>
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="outline" disabled={!!loading}>
|
||||
{loading === "tone" ? <CircleNotch className="animate-spin" /> : <ChatTeardropText />}
|
||||
<span className="mx-2 text-xs">Change Tone</span>
|
||||
<span className="mx-2 text-xs">{t`Change Tone`}</span>
|
||||
<CaretDown />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => onClick("tone", "casual")}>
|
||||
<span role="img" aria-label="Casual">
|
||||
<span role="img" aria-label={t`Casual`}>
|
||||
🙂
|
||||
</span>
|
||||
<span className="ml-2">Casual</span>
|
||||
<span className="ml-2">{t`Casual`}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onClick("tone", "professional")}>
|
||||
<span role="img" aria-label="Professional">
|
||||
<span role="img" aria-label={t`Professional`}>
|
||||
💼
|
||||
</span>
|
||||
<span className="ml-2">Professional</span>
|
||||
<span className="ml-2">{t`Professional`}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onClick("tone", "confident")}>
|
||||
<span role="img" aria-label="Confident">
|
||||
<span role="img" aria-label={t`Confident`}>
|
||||
😎
|
||||
</span>
|
||||
<span className="ml-2">Confident</span>
|
||||
<span className="ml-2">{t`Confident`}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onClick("tone", "friendly")}>
|
||||
<span role="img" aria-label="Friendly">
|
||||
<span role="img" aria-label={t`Friendly`}>
|
||||
😊
|
||||
</span>
|
||||
<span className="ml-2">Friendly</span>
|
||||
<span className="ml-2">{t`Friendly`}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
|
||||
type Props = {
|
||||
@ -12,23 +13,29 @@ export const Copyright = ({ className }: Props) => (
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
Licensed under{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
href="https://github.com/AmruthPillai/Reactive-Resume/blob/main/LICENSE"
|
||||
>
|
||||
MIT
|
||||
</a>
|
||||
<Trans>
|
||||
Licensed under{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
href="https://github.com/AmruthPillai/Reactive-Resume/blob/main/LICENSE"
|
||||
>
|
||||
MIT
|
||||
</a>
|
||||
</Trans>
|
||||
</span>
|
||||
<span>By the community, for the community.</span>
|
||||
<span>{t`By the community, for the community.`}</span>
|
||||
<span>
|
||||
A passion project by{" "}
|
||||
<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.amruthpillai.com/">
|
||||
Amruth Pillai
|
||||
</a>
|
||||
<Trans>
|
||||
A passion project by{" "}
|
||||
<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.amruthpillai.com/">
|
||||
Amruth Pillai
|
||||
</a>
|
||||
</Trans>
|
||||
</span>
|
||||
|
||||
<span className="mt-2 font-bold">Reactive Resume v{appVersion}</span>
|
||||
<span className="mt-4">
|
||||
{t`Reactive Resume`} {"v" + appVersion}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { useTheme } from "@reactive-resume/hooks";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
|
||||
@ -25,7 +26,7 @@ export const Icon = ({ size = 32, className }: Props) => {
|
||||
src={src}
|
||||
width={size}
|
||||
height={size}
|
||||
alt="Reactive Resume"
|
||||
alt={t`Reactive Resume`}
|
||||
className={cn("rounded-sm", className)}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { useTheme } from "@reactive-resume/hooks";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
|
||||
@ -25,7 +26,7 @@ export const Logo = ({ size = 32, className }: Props) => {
|
||||
src={src}
|
||||
width={size}
|
||||
height={size}
|
||||
alt="Reactive Resume"
|
||||
alt={t`Reactive Resume`}
|
||||
className={cn("rounded-sm", className)}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -24,12 +25,14 @@ export const UserOptions = ({ children }: Props) => {
|
||||
|
||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||
<DropdownMenuItem onClick={() => navigate("/dashboard/settings")}>
|
||||
Settings
|
||||
{t`Settings`}
|
||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||
<KeyboardShortcut>⇧S</KeyboardShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => logout()}>
|
||||
Logout
|
||||
{t`Logout`}
|
||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||
<KeyboardShortcut>⇧Q</KeyboardShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
16
apps/client/src/libs/lingui.ts
Normal file
16
apps/client/src/libs/lingui.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { i18n } from "@lingui/core";
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
export const getLocales = () => ({
|
||||
"en-US": t`English`,
|
||||
"de-DE": t`German`,
|
||||
});
|
||||
|
||||
export const defaultLocale = "en-US";
|
||||
|
||||
export async function dynamicActivate(locale: string) {
|
||||
const { messages } = await import(`../locales/${locale}.po`);
|
||||
|
||||
i18n.load(locale, messages);
|
||||
i18n.activate(locale);
|
||||
}
|
||||
1481
apps/client/src/locales/de-DE.po
Normal file
1481
apps/client/src/locales/de-DE.po
Normal file
File diff suppressed because it is too large
Load Diff
1477
apps/client/src/locales/en-US.po
Normal file
1477
apps/client/src/locales/en-US.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { GithubLogo, GoogleLogo } from "@phosphor-icons/react";
|
||||
import { Button } from "@reactive-resume/ui";
|
||||
|
||||
@ -5,15 +6,15 @@ export const SocialAuth = () => (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Button asChild size="lg" className="w-full !bg-[#222] !text-white hover:!bg-[#222]/80">
|
||||
<a href="/api/auth/github">
|
||||
<GoogleLogo className="mr-3 h-4 w-4" />
|
||||
GitHub
|
||||
<GithubLogo className="mr-3 h-4 w-4" />
|
||||
{t`GitHub`}
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button asChild size="lg" className="w-full !bg-[#4285F4] !text-white hover:!bg-[#4285F4]/80">
|
||||
<a href="/api/auth/google">
|
||||
<GithubLogo className="mr-3 h-4 w-4" />
|
||||
Google
|
||||
<GoogleLogo className="mr-3 h-4 w-4" />
|
||||
{t`Google`}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { Warning } from "@phosphor-icons/react";
|
||||
import { twoFactorBackupSchema } from "@reactive-resume/dto";
|
||||
import { usePasswordToggle } from "@reactive-resume/hooks";
|
||||
@ -49,7 +50,7 @@ export const BackupOtpPage = () => {
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to sign in",
|
||||
title: t`An error occurred while trying to sign in to your account.`,
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
@ -59,9 +60,9 @@ export const BackupOtpPage = () => {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Use your backup code</h2>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">{t`Use your backup code`}</h2>
|
||||
<h6 className="leading-relaxed opacity-60">
|
||||
Enter one of the 10 backup codes you saved when you enabled two-factor authentication.
|
||||
{t`Enter one of the 10 backup codes you saved when you enabled two-factor authentication.`}
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
@ -77,12 +78,12 @@ export const BackupOtpPage = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Backup Code</FormLabel>
|
||||
<FormLabel>{t`Backup Code`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
pattern="[a-z0-9]{10}"
|
||||
placeholder="a1b2c3d4e5"
|
||||
title="may contain lowercase letters or numbers, and must be exactly 10 characters."
|
||||
title={t`Backup Codes may contain only lowercase letters or numbers, and must be exactly 10 characters.`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@ -92,7 +93,7 @@ export const BackupOtpPage = () => {
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={loading} className="mt-4 w-full">
|
||||
Sign in
|
||||
{t`Sign in`}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { Warning } from "@phosphor-icons/react";
|
||||
import { forgotPasswordSchema } from "@reactive-resume/dto";
|
||||
import {
|
||||
@ -44,7 +45,7 @@ export const ForgotPasswordPage = () => {
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to send your password recovery email",
|
||||
title: t`An error occurred while trying to send your password recovery email.`,
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
@ -55,11 +56,10 @@ export const ForgotPasswordPage = () => {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">You've got mail!</h2>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">{t`You've got mail!`}</h2>
|
||||
<Alert variant="success">
|
||||
<AlertDescription className="pt-0">
|
||||
A password reset link should have been sent to your inbox, if an account existed with
|
||||
the email you provided.
|
||||
{t`A password reset link should have been sent to your inbox, if an account existed with the email you provided.`}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@ -70,10 +70,9 @@ export const ForgotPasswordPage = () => {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Forgot your password?</h2>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">{t`Forgot your password?`}</h2>
|
||||
<h6 className="leading-relaxed opacity-75">
|
||||
Enter your email address and we will send you a link to reset your password if the account
|
||||
exists.
|
||||
{t`Enter your email address and we will send you a link to reset your password if the account exists.`}
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
@ -85,7 +84,7 @@ export const ForgotPasswordPage = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>{t`Email`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="john.doe@example.com" {...field} />
|
||||
</FormControl>
|
||||
@ -95,7 +94,7 @@ export const ForgotPasswordPage = () => {
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={loading} className="mt-4 w-full">
|
||||
Send Email
|
||||
{t`Send Email`}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { useMemo } from "react";
|
||||
import { Link, matchRoutes, Outlet, useLocation } from "react-router-dom";
|
||||
|
||||
@ -25,7 +26,13 @@ export const AuthLayout = () => {
|
||||
<>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<hr className="flex-1" />
|
||||
<span className="text-xs font-medium">or continue with</span>
|
||||
<span className="text-xs font-medium">
|
||||
{t({
|
||||
message: "or continue with",
|
||||
context:
|
||||
"The user can either login with email/password, or continue with GitHub or Google.",
|
||||
})}
|
||||
</span>
|
||||
<hr className="flex-1" />
|
||||
</div>
|
||||
|
||||
@ -38,18 +45,18 @@ export const AuthLayout = () => {
|
||||
<img
|
||||
width={1920}
|
||||
height={1080}
|
||||
alt="Open books on a table"
|
||||
alt={t`Open books on a table`}
|
||||
className="h-screen w-full object-cover object-center"
|
||||
src="/backgrounds/patrick-tomasso-Oaqk7qqNh_c-unsplash.jpg"
|
||||
/>
|
||||
|
||||
<div className="absolute bottom-5 right-5 z-10 bg-primary/30 px-4 py-2 text-xs font-medium text-primary-foreground backdrop-blur-sm">
|
||||
<a
|
||||
href="https://unsplash.com/photos/Oaqk7qqNh_c"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
href="https://unsplash.com/photos/Oaqk7qqNh_c"
|
||||
>
|
||||
Photograph by Patrick Tomasso
|
||||
{t`Photograph by Patrick Tomasso`}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { ArrowRight, Warning } from "@phosphor-icons/react";
|
||||
import { loginSchema } from "@reactive-resume/dto";
|
||||
import { usePasswordToggle } from "@reactive-resume/hooks";
|
||||
@ -48,7 +49,7 @@ export const LoginPage = () => {
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to sign in",
|
||||
title: t`An error occurred while trying to sign in to your account.`,
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
@ -58,12 +59,13 @@ export const LoginPage = () => {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Sign in to your account</h2>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">{t`Sign in to your account`}</h2>
|
||||
<h6>
|
||||
<span className="opacity-75">Don't have an account?</span>
|
||||
<span className="opacity-75">{t`Don't have an account?`}</span>
|
||||
<Button asChild variant="link" className="px-1.5">
|
||||
<Link to="/auth/register">
|
||||
Create one now <ArrowRight className="ml-1" />
|
||||
{t({ message: "Create one now", context: "This is a link to create a new account" })}{" "}
|
||||
<ArrowRight className="ml-1" />
|
||||
</Link>
|
||||
</Button>
|
||||
</h6>
|
||||
@ -81,11 +83,11 @@ export const LoginPage = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>{t`Email`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="john.doe@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>You can also enter your username.</FormDescription>
|
||||
<FormDescription>{t`You can also enter your username.`}</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@ -96,13 +98,15 @@ export const LoginPage = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>{t`Password`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
|
||||
temporarily.
|
||||
<Trans>
|
||||
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
|
||||
temporarily.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -111,11 +115,11 @@ export const LoginPage = () => {
|
||||
|
||||
<div className="mt-4 flex items-center gap-x-4">
|
||||
<Button type="submit" disabled={loading} className="flex-1">
|
||||
Sign in
|
||||
{t`Sign in`}
|
||||
</Button>
|
||||
|
||||
<Button asChild variant="link" className="px-4">
|
||||
<Link to="/auth/forgot-password">Forgot Password?</Link>
|
||||
<Link to="/auth/forgot-password">{t`Forgot Password?`}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { ArrowRight, Warning } from "@phosphor-icons/react";
|
||||
import { registerSchema } from "@reactive-resume/dto";
|
||||
import { usePasswordToggle } from "@reactive-resume/hooks";
|
||||
@ -38,7 +39,7 @@ export const RegisterPage = () => {
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
language: "en",
|
||||
locale: "en",
|
||||
},
|
||||
});
|
||||
|
||||
@ -56,7 +57,7 @@ export const RegisterPage = () => {
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to sign up",
|
||||
title: t`An error occurred while trying to create a new account.`,
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
@ -66,12 +67,12 @@ export const RegisterPage = () => {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Create a new account</h2>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">{t`Create a new account`}</h2>
|
||||
<h6>
|
||||
<span className="opacity-75">Already have an account?</span>
|
||||
<span className="opacity-75">{t`Already have an account?`}</span>
|
||||
<Button asChild variant="link" className="px-1.5">
|
||||
<Link to="/auth/login">
|
||||
Sign in now <ArrowRight className="ml-1" />
|
||||
{t`Sign in now`} <ArrowRight className="ml-1" />
|
||||
</Link>
|
||||
</Button>
|
||||
</h6>
|
||||
@ -89,9 +90,16 @@ export const RegisterPage = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t`Name`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="John Doe" {...field} />
|
||||
<Input
|
||||
placeholder={t({
|
||||
message: "John Doe",
|
||||
context:
|
||||
"Localized version of a placeholder name. For example, Max Mustermann in German or Jan Kowalski in Polish.",
|
||||
})}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -103,9 +111,16 @@ export const RegisterPage = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormLabel>{t`Username`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="john.doe" {...field} />
|
||||
<Input
|
||||
placeholder={t({
|
||||
message: "john.doe",
|
||||
context:
|
||||
"Localized version of a placeholder username. For example, max.mustermann in German or jan.kowalski in Polish.",
|
||||
})}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -117,9 +132,16 @@ export const RegisterPage = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>{t`Email`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="john.doe@example.com" {...field} />
|
||||
<Input
|
||||
placeholder={t({
|
||||
message: "john.doe@example.com",
|
||||
context:
|
||||
"Localized version of a placeholder email. For example, max.mustermann@example.de in German or jan.kowalski@example.pl in Polish.",
|
||||
})}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -131,13 +153,15 @@ export const RegisterPage = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>{t`Password`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
|
||||
temporarily.
|
||||
<Trans>
|
||||
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
|
||||
temporarily.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -145,7 +169,7 @@ export const RegisterPage = () => {
|
||||
/>
|
||||
|
||||
<Button disabled={loading} className="mt-4 w-full">
|
||||
Sign up
|
||||
{t`Sign up`}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { Warning } from "@phosphor-icons/react";
|
||||
import { resetPasswordSchema } from "@reactive-resume/dto";
|
||||
import { usePasswordToggle } from "@reactive-resume/hooks";
|
||||
@ -53,7 +54,7 @@ export const ResetPasswordPage = () => {
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to reset your password",
|
||||
title: t`An error occurred while trying to reset your password.`,
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
@ -68,9 +69,9 @@ export const ResetPasswordPage = () => {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Reset your password</h2>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">{t`Reset your password`}</h2>
|
||||
<h6 className="leading-relaxed opacity-75">
|
||||
Enter a new password below, and make sure it's secure.
|
||||
{t`Enter a new password below, and make sure it's secure.`}
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
@ -86,13 +87,15 @@ export const ResetPasswordPage = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>{t`Password`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
|
||||
temporarily.
|
||||
<Trans>
|
||||
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
|
||||
temporarily.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -100,7 +103,7 @@ export const ResetPasswordPage = () => {
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={loading} className="mt-4 w-full">
|
||||
Update Password
|
||||
{t`Change Password`}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { ArrowRight, Info, SealCheck, Warning } from "@phosphor-icons/react";
|
||||
import { Alert, AlertDescription, AlertTitle, Button } from "@reactive-resume/ui";
|
||||
import { AxiosError } from "axios";
|
||||
@ -25,7 +26,7 @@ export const VerifyEmailPage = () => {
|
||||
toast({
|
||||
variant: "success",
|
||||
icon: <SealCheck size={16} weight="bold" />,
|
||||
title: "Your email address has been verified successfully.",
|
||||
title: t`Your email address has been verified successfully.`,
|
||||
});
|
||||
|
||||
navigate("/dashboard/resumes", { replace: true });
|
||||
@ -36,7 +37,7 @@ export const VerifyEmailPage = () => {
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to verify your email address",
|
||||
title: t`An error occurred while trying to verify your email address.`,
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
@ -51,27 +52,26 @@ export const VerifyEmailPage = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Verify your email address</h2>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">{t`Verify your email address`}</h2>
|
||||
<p className="leading-relaxed opacity-75">
|
||||
You should have received an email from <strong>Reactive Resume</strong> with a link to
|
||||
verify your account.
|
||||
<Trans>
|
||||
You should have received an email from <strong>Reactive Resume</strong> with a link to
|
||||
verify your account.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Alert variant="info">
|
||||
<Info size={18} />
|
||||
|
||||
<AlertTitle>Please note that this step is completely optional.</AlertTitle>
|
||||
|
||||
<AlertTitle>{t`Please note that this step is completely optional.`}</AlertTitle>
|
||||
<AlertDescription>
|
||||
We verify your email address only to ensure that we can send you a password reset link in
|
||||
case you forget your password.
|
||||
{t`We verify your email address only to ensure that we can send you a password reset link in case you forget your password.`}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button asChild disabled={loading}>
|
||||
<Link to="/dashboard">
|
||||
Continue to Dashboard
|
||||
{t`Go to Dashboard`}
|
||||
<ArrowRight className="ml-2" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { ArrowRight, Warning } from "@phosphor-icons/react";
|
||||
import { twoFactorSchema } from "@reactive-resume/dto";
|
||||
import { usePasswordToggle } from "@reactive-resume/hooks";
|
||||
@ -49,7 +50,7 @@ export const VerifyOtpPage = () => {
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while trying to sign in",
|
||||
title: t`An error occurred while trying to sign in to your account.`,
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
@ -59,14 +60,14 @@ export const VerifyOtpPage = () => {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1.5">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Two Step Verification</h2>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">{t`Two-Factor Authentication`}</h2>
|
||||
<h6>
|
||||
<span className="opacity-75">
|
||||
Enter the one-time password provided by your authenticator app below.
|
||||
{t`Enter the one-time password provided by your authenticator app below.`}
|
||||
</span>
|
||||
<Button asChild variant="link" className="px-1.5">
|
||||
<Link to="/auth/backup-otp">
|
||||
Lost your device? <ArrowRight className="ml-1" />
|
||||
{t`Lost your device?`} <ArrowRight className="ml-1" />
|
||||
</Link>
|
||||
</Button>
|
||||
</h6>
|
||||
@ -84,7 +85,7 @@ export const VerifyOtpPage = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>One-Time Password</FormLabel>
|
||||
<FormLabel>{t`One-Time Password`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="123456" {...field} />
|
||||
</FormControl>
|
||||
@ -94,7 +95,7 @@ export const VerifyOtpPage = () => {
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={loading} className="mt-4 w-full">
|
||||
Sign in
|
||||
{t`Sign in`}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { HouseSimple, Lock, SidebarSimple } from "@phosphor-icons/react";
|
||||
import { useBreakpoint } from "@reactive-resume/hooks";
|
||||
import { Button, Tooltip } from "@reactive-resume/ui";
|
||||
@ -52,7 +53,7 @@ export const BuilderHeader = () => {
|
||||
<h1 className="font-medium">{title}</h1>
|
||||
|
||||
{locked && (
|
||||
<Tooltip content="This resume is locked, please unlock to make further changes.">
|
||||
<Tooltip content={t`This resume is locked, please unlock to make further changes.`}>
|
||||
<Lock size={14} className="ml-2 opacity-75" />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import {
|
||||
ArrowClockwise,
|
||||
ArrowCounterClockwise,
|
||||
@ -55,14 +56,14 @@ export const BuilderToolbar = () => {
|
||||
>
|
||||
<div className="inline-flex items-center justify-center rounded-full bg-background px-4 shadow-xl">
|
||||
{/* Undo */}
|
||||
<Tooltip content="Undo">
|
||||
<Tooltip content={t`Undo`}>
|
||||
<Button size="icon" variant="ghost" className="rounded-none" onClick={() => undo()}>
|
||||
<ArrowCounterClockwise />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{/* Redo */}
|
||||
<Tooltip content="Redo">
|
||||
<Tooltip content={t`Redo`}>
|
||||
<Button size="icon" variant="ghost" className="rounded-none" onClick={() => redo()}>
|
||||
<ArrowClockwise />
|
||||
</Button>
|
||||
@ -70,25 +71,25 @@ export const BuilderToolbar = () => {
|
||||
|
||||
<Separator orientation="vertical" className="h-9" />
|
||||
|
||||
<Tooltip content="Zoom In">
|
||||
<Tooltip content={t`Zoom In`}>
|
||||
<Button size="icon" variant="ghost" className="rounded-none" onClick={onZoomIn}>
|
||||
<MagnifyingGlassPlus />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Zoom Out">
|
||||
<Tooltip content={t`Zoom Out`}>
|
||||
<Button size="icon" variant="ghost" className="rounded-none" onClick={onZoomOut}>
|
||||
<MagnifyingGlassMinus />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Reset Zoom">
|
||||
<Tooltip content={t`Reset Zoom`}>
|
||||
<Button size="icon" variant="ghost" className="rounded-none" onClick={onResetView}>
|
||||
<ClockClockwise />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Center Artboard">
|
||||
<Tooltip content={t`Center Artboard`}>
|
||||
<Button size="icon" variant="ghost" className="rounded-none" onClick={onCenterView}>
|
||||
<CubeFocus />
|
||||
</Button>
|
||||
@ -97,7 +98,7 @@ export const BuilderToolbar = () => {
|
||||
<Separator orientation="vertical" className="h-9" />
|
||||
|
||||
{/* Toggle Page Break Line */}
|
||||
<Tooltip content="Toggle Page Break Line">
|
||||
<Tooltip content={t`Toggle Page Break Line`}>
|
||||
<Toggle
|
||||
className="rounded-none"
|
||||
pressed={pageOptions.breakLine}
|
||||
@ -110,7 +111,7 @@ export const BuilderToolbar = () => {
|
||||
</Tooltip>
|
||||
|
||||
{/* Toggle Page Numbers */}
|
||||
<Tooltip content="Toggle Page Numbers">
|
||||
<Tooltip content={t`Toggle Page Numbers`}>
|
||||
<Toggle
|
||||
className="rounded-none"
|
||||
pressed={pageOptions.pageNumbers}
|
||||
@ -125,14 +126,14 @@ export const BuilderToolbar = () => {
|
||||
<Separator orientation="vertical" className="h-9" />
|
||||
|
||||
{/* Copy Link to Resume */}
|
||||
<Tooltip content="Copy Link to Resume">
|
||||
<Tooltip content={t`Copy Link to Resume`}>
|
||||
<Button size="icon" variant="ghost" className="rounded-none" disabled={!isPublic}>
|
||||
<LinkSimple />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{/* Download PDF */}
|
||||
<Tooltip content="Download PDF">
|
||||
<Tooltip content={t`Download PDF`}>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { ResumeDto } from "@reactive-resume/dto";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
@ -34,7 +35,9 @@ export const BuilderPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{title} - Reactive Resume</title>
|
||||
<title>
|
||||
{title} - {t`Reactive Resume`}
|
||||
</title>
|
||||
</Helmet>
|
||||
|
||||
<iframe
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { awardSchema, defaultAward } from "@reactive-resume/schema";
|
||||
import {
|
||||
FormControl,
|
||||
@ -35,9 +36,9 @@ export const AwardsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormLabel>{t({ message: "Title", context: "Name of the Award" })}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="3rd Runner Up" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -49,9 +50,9 @@ export const AwardsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Awarder</FormLabel>
|
||||
<FormLabel>{t`Awarder`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="TechCrunch Disrupt SF" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -63,9 +64,15 @@ export const AwardsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Date</FormLabel>
|
||||
<FormLabel>{t`Date`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Aug 2019" />
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t({
|
||||
message: "March 2023",
|
||||
comment: "The month and year should be uniform across all languages.",
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -77,9 +84,9 @@ export const AwardsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Website</FormLabel>
|
||||
<FormLabel>{t`Website`}</FormLabel>
|
||||
<FormControl>
|
||||
<URLInput {...field} placeholder="https://techcrunch.com/events/disrupt-sf-2019" />
|
||||
<URLInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -91,7 +98,7 @@ export const AwardsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Summary</FormLabel>
|
||||
<FormLabel>{t`Summary`}</FormLabel>
|
||||
<FormControl>
|
||||
<RichInput
|
||||
{...field}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { certificationSchema, defaultCertification } from "@reactive-resume/schema";
|
||||
import {
|
||||
FormControl,
|
||||
@ -35,9 +36,9 @@ export const CertificationsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t({ message: "Name", context: "Name of the Certification" })}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Web Developer Bootcamp" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -49,9 +50,9 @@ export const CertificationsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Issuer</FormLabel>
|
||||
<FormLabel>{t`Issuer`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Udemy" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -63,9 +64,9 @@ export const CertificationsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Date</FormLabel>
|
||||
<FormLabel>{t`Date`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Aug 2019" />
|
||||
<Input {...field} placeholder={t`March 2023`} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -77,7 +78,7 @@ export const CertificationsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Website</FormLabel>
|
||||
<FormLabel>{t`Website`}</FormLabel>
|
||||
<FormControl>
|
||||
<URLInput {...field} placeholder="https://udemy.com/certificate/UC-..." />
|
||||
</FormControl>
|
||||
@ -91,7 +92,7 @@ export const CertificationsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Summary</FormLabel>
|
||||
<FormLabel>{t`Summary`}</FormLabel>
|
||||
<FormControl>
|
||||
<RichInput
|
||||
{...field}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { CustomSection, customSectionSchema, defaultCustomSection } from "@reactive-resume/schema";
|
||||
import {
|
||||
@ -49,7 +50,7 @@ export const CustomSectionDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t`Name`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@ -63,7 +64,7 @@ export const CustomSectionDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormLabel>{t`Description`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@ -77,7 +78,7 @@ export const CustomSectionDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Date</FormLabel>
|
||||
<FormLabel>{t`Date`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@ -91,7 +92,7 @@ export const CustomSectionDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Location</FormLabel>
|
||||
<FormLabel>{t`Location`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@ -105,7 +106,7 @@ export const CustomSectionDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Website</FormLabel>
|
||||
<FormLabel>{t`Website`}</FormLabel>
|
||||
<FormControl>
|
||||
<URLInput {...field} />
|
||||
</FormControl>
|
||||
@ -119,7 +120,7 @@ export const CustomSectionDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Summary</FormLabel>
|
||||
<FormLabel>{t`Summary`}</FormLabel>
|
||||
<FormControl>
|
||||
<RichInput
|
||||
{...field}
|
||||
@ -141,12 +142,12 @@ export const CustomSectionDialog = () => {
|
||||
render={({ field }) => (
|
||||
<div className="col-span-2 space-y-3">
|
||||
<FormItem>
|
||||
<FormLabel>Keywords</FormLabel>
|
||||
<FormLabel>{t`Keywords`}</FormLabel>
|
||||
<FormControl>
|
||||
<BadgeInput {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
You can add multiple keywords by separating them with a comma.
|
||||
{t`You can add multiple keywords by separating them with a comma or pressing enter.`}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { defaultEducation, educationSchema } from "@reactive-resume/schema";
|
||||
import {
|
||||
FormControl,
|
||||
@ -35,9 +36,9 @@ export const EducationDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Institution</FormLabel>
|
||||
<FormLabel>{t`Institution`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Carnegie Mellon University" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -49,9 +50,14 @@ export const EducationDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Type of Study</FormLabel>
|
||||
<FormLabel>
|
||||
{t({
|
||||
message: "Type of Study",
|
||||
comment: "For example, Bachelor's Degree or Master's Degree",
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Bachelor's Degree" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -63,9 +69,14 @@ export const EducationDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Area of Study</FormLabel>
|
||||
<FormLabel>
|
||||
{t({
|
||||
message: "Area of Study",
|
||||
comment: "For example, Computer Science or Business Administration",
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Computer Science" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -77,7 +88,12 @@ export const EducationDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Score</FormLabel>
|
||||
<FormLabel>
|
||||
{t({
|
||||
message: "Score",
|
||||
comment: "Score or honors for the degree, for example, CGPA or magna cum laude",
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="9.2 GPA" />
|
||||
</FormControl>
|
||||
@ -91,9 +107,9 @@ export const EducationDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Date</FormLabel>
|
||||
<FormLabel>{t`Date`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Aug 2006 - Oct 2012" />
|
||||
<Input {...field} placeholder={t`March 2023 - Present`} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -105,9 +121,9 @@ export const EducationDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Website</FormLabel>
|
||||
<FormLabel>{t`Website`}</FormLabel>
|
||||
<FormControl>
|
||||
<URLInput {...field} placeholder="https://www.cmu.edu/" />
|
||||
<URLInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -119,7 +135,7 @@ export const EducationDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Summary</FormLabel>
|
||||
<FormLabel>{t`Summary`}</FormLabel>
|
||||
<FormControl>
|
||||
<RichInput
|
||||
{...field}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { defaultExperience, experienceSchema } from "@reactive-resume/schema";
|
||||
import {
|
||||
FormControl,
|
||||
@ -35,9 +36,9 @@ export const ExperienceDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Company</FormLabel>
|
||||
<FormLabel>{t`Company`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Alphabet Inc." />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -49,9 +50,14 @@ export const ExperienceDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Position</FormLabel>
|
||||
<FormLabel>
|
||||
{t({
|
||||
message: "Position",
|
||||
context: "Position held at a company, for example, Software Engineer",
|
||||
})}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Chief Executive Officer" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -63,9 +69,9 @@ export const ExperienceDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Date</FormLabel>
|
||||
<FormLabel>{t`Date`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Dec 2019 - Present" />
|
||||
<Input {...field} placeholder={t`March 2023 - Present`} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -77,9 +83,9 @@ export const ExperienceDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Location</FormLabel>
|
||||
<FormLabel>{t`Location`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="New York, NY" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -91,9 +97,9 @@ export const ExperienceDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Website</FormLabel>
|
||||
<FormLabel>{t`Website`}</FormLabel>
|
||||
<FormControl>
|
||||
<URLInput {...field} placeholder="https://www.abc.xyz/" />
|
||||
<URLInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -105,7 +111,7 @@ export const ExperienceDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Summary</FormLabel>
|
||||
<FormLabel>{t`Summary`}</FormLabel>
|
||||
<FormControl>
|
||||
<RichInput
|
||||
{...field}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { defaultInterest, interestSchema } from "@reactive-resume/schema";
|
||||
import {
|
||||
@ -36,9 +37,9 @@ export const InterestsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t`Name`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Video Games" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -51,12 +52,12 @@ export const InterestsDialog = () => {
|
||||
render={({ field }) => (
|
||||
<div className="col-span-2 space-y-3">
|
||||
<FormItem>
|
||||
<FormLabel>Keywords</FormLabel>
|
||||
<FormLabel>{t`Keywords`}</FormLabel>
|
||||
<FormControl>
|
||||
<BadgeInput {...field} placeholder="FIFA 23, Call of Duty, etc." />
|
||||
<BadgeInput {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
You can add multiple keywords by separating them with a comma.
|
||||
{t`You can add multiple keywords by separating them with a comma or pressing enter.`}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { defaultLanguage, languageSchema } from "@reactive-resume/schema";
|
||||
import {
|
||||
FormControl,
|
||||
@ -33,9 +34,9 @@ export const LanguagesDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t`Name`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="German" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -47,9 +48,9 @@ export const LanguagesDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Fluency</FormLabel>
|
||||
<FormLabel>{t`Fluency`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Native Speaker" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -61,7 +62,7 @@ export const LanguagesDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Fluency (CEFR)</FormLabel>
|
||||
<FormLabel>{t`Fluency (CEFR)`}</FormLabel>
|
||||
<FormControl className="py-2">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Slider
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { defaultProfile, profileSchema } from "@reactive-resume/schema";
|
||||
import {
|
||||
Avatar,
|
||||
@ -35,8 +36,9 @@ export const ProfilesDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Network</FormLabel>
|
||||
<FormLabel>{t`Network`}</FormLabel>
|
||||
<FormControl>
|
||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||
<Input {...field} placeholder="LinkedIn" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@ -49,9 +51,9 @@ export const ProfilesDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormLabel>{t`Username`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="johndoe" />
|
||||
<Input {...field} placeholder="john.doe" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -63,7 +65,7 @@ export const ProfilesDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>URL</FormLabel>
|
||||
<FormLabel>{t`Website`}</FormLabel>
|
||||
<FormControl>
|
||||
<URLInput {...field} placeholder="https://linkedin.com/in/johndoe" />
|
||||
</FormControl>
|
||||
@ -77,7 +79,7 @@ export const ProfilesDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel htmlFor="iconSlug">Icon</FormLabel>
|
||||
<FormLabel htmlFor="iconSlug">{t`Icon`}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Avatar className="h-8 w-8 bg-white">
|
||||
@ -93,15 +95,17 @@ export const ProfilesDialog = () => {
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription className="ml-10">
|
||||
Powered by{" "}
|
||||
<a
|
||||
href="https://simpleicons.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
className="font-medium"
|
||||
>
|
||||
Simple Icons
|
||||
</a>
|
||||
<Trans>
|
||||
Powered by{" "}
|
||||
<a
|
||||
href="https://simpleicons.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
className="font-medium"
|
||||
>
|
||||
Simple Icons
|
||||
</a>
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { defaultProject, projectSchema } from "@reactive-resume/schema";
|
||||
import {
|
||||
@ -40,9 +41,9 @@ export const ProjectsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t`Name`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Reactive Resume" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -54,9 +55,9 @@ export const ProjectsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormLabel>{t`Description`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Open Source Resume Builder" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -68,9 +69,9 @@ export const ProjectsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Date</FormLabel>
|
||||
<FormLabel>{t`Date`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Sep 2018 - Present" />
|
||||
<Input {...field} placeholder={t`March 2023 - Present`} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -82,7 +83,7 @@ export const ProjectsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Website</FormLabel>
|
||||
<FormLabel>{t`Website`}</FormLabel>
|
||||
<FormControl>
|
||||
<URLInput {...field} placeholder="https://rxresu.me" />
|
||||
</FormControl>
|
||||
@ -96,7 +97,7 @@ export const ProjectsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Summary</FormLabel>
|
||||
<FormLabel>{t`Summary`}</FormLabel>
|
||||
<FormControl>
|
||||
<RichInput
|
||||
{...field}
|
||||
@ -118,12 +119,12 @@ export const ProjectsDialog = () => {
|
||||
render={({ field }) => (
|
||||
<div className="col-span-2 space-y-3">
|
||||
<FormItem>
|
||||
<FormLabel>Keywords</FormLabel>
|
||||
<FormLabel>{t`Keywords`}</FormLabel>
|
||||
<FormControl>
|
||||
<BadgeInput {...field} placeholder="FIFA 23, Call of Duty, etc." />
|
||||
<BadgeInput {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
You can add multiple keywords by separating them with a comma.
|
||||
{t`You can add multiple keywords by separating them with a comma or pressing enter.`}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { defaultPublication, publicationSchema } from "@reactive-resume/schema";
|
||||
import {
|
||||
FormControl,
|
||||
@ -35,9 +36,9 @@ export const PublicationsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t`Name`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="The Great Gatsby" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -49,9 +50,9 @@ export const PublicationsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Publisher</FormLabel>
|
||||
<FormLabel>{t`Publisher`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Charles Scribner's Sons" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -63,9 +64,9 @@ export const PublicationsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Release Date</FormLabel>
|
||||
<FormLabel>{t`Date`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="April 10, 1925" />
|
||||
<Input {...field} placeholder={t`March 2023`} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -77,9 +78,9 @@ export const PublicationsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Website</FormLabel>
|
||||
<FormLabel>{t`Website`}</FormLabel>
|
||||
<FormControl>
|
||||
<URLInput {...field} placeholder="https://books.google.com/..." />
|
||||
<URLInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -91,7 +92,7 @@ export const PublicationsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Summary</FormLabel>
|
||||
<FormLabel>{t`Summary`}</FormLabel>
|
||||
<FormControl>
|
||||
<RichInput
|
||||
{...field}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { defaultReference, referenceSchema } from "@reactive-resume/schema";
|
||||
import {
|
||||
FormControl,
|
||||
@ -35,9 +36,9 @@ export const ReferencesDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t`Name`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Cosmo Kramer" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -49,9 +50,9 @@ export const ReferencesDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormLabel>{t`Description`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Neighbour" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -63,9 +64,9 @@ export const ReferencesDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Website</FormLabel>
|
||||
<FormLabel>{t`Website`}</FormLabel>
|
||||
<FormControl>
|
||||
<URLInput {...field} placeholder="https://linkedin.com/in/cosmo.kramer" />
|
||||
<URLInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -77,7 +78,7 @@ export const ReferencesDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Summary</FormLabel>
|
||||
<FormLabel>{t`Summary`}</FormLabel>
|
||||
<FormControl>
|
||||
<RichInput
|
||||
{...field}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { defaultSkill, skillSchema } from "@reactive-resume/schema";
|
||||
import {
|
||||
@ -37,9 +38,9 @@ export const SkillsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t`Name`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Content Management" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -51,9 +52,9 @@ export const SkillsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormLabel>{t`Description`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Advanced" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -65,7 +66,7 @@ export const SkillsDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Level</FormLabel>
|
||||
<FormLabel>{t`Level`}</FormLabel>
|
||||
<FormControl className="py-2">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Slider
|
||||
@ -91,12 +92,12 @@ export const SkillsDialog = () => {
|
||||
render={({ field }) => (
|
||||
<div className="col-span-2 space-y-3">
|
||||
<FormItem>
|
||||
<FormLabel>Keywords</FormLabel>
|
||||
<FormLabel>{t`Keywords`}</FormLabel>
|
||||
<FormControl>
|
||||
<BadgeInput {...field} placeholder="WordPress, Joomla, Webflow etc." />
|
||||
<BadgeInput {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
You can add multiple keywords by separating them with a comma.
|
||||
{t`You can add multiple keywords by separating them with a comma or pressing enter.`}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { defaultVolunteer, volunteerSchema } from "@reactive-resume/schema";
|
||||
import {
|
||||
FormControl,
|
||||
@ -35,9 +36,9 @@ export const VolunteerDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Organization</FormLabel>
|
||||
<FormLabel>{t`Organization`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Amnesty International" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -49,9 +50,9 @@ export const VolunteerDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Position</FormLabel>
|
||||
<FormLabel>{t`Position`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Recruiter" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -63,9 +64,9 @@ export const VolunteerDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Date</FormLabel>
|
||||
<FormLabel>{t`Date`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Dec 2016 - Aug 2017" />
|
||||
<Input {...field} placeholder={t`March 2023 - Present`} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -77,9 +78,9 @@ export const VolunteerDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1">
|
||||
<FormLabel>Location</FormLabel>
|
||||
<FormLabel>{t`Location`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="New York, NY" />
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -91,9 +92,9 @@ export const VolunteerDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Website</FormLabel>
|
||||
<FormLabel>{t`Website`}</FormLabel>
|
||||
<FormControl>
|
||||
<URLInput {...field} placeholder="https://www.amnesty.org/" />
|
||||
<URLInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -105,7 +106,7 @@ export const VolunteerDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-1 sm:col-span-2">
|
||||
<FormLabel>Summary</FormLabel>
|
||||
<FormLabel>{t`Summary`}</FormLabel>
|
||||
<FormControl>
|
||||
<RichInput
|
||||
{...field}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { Plus, PlusCircle } from "@phosphor-icons/react";
|
||||
import {
|
||||
Award,
|
||||
@ -50,7 +51,15 @@ export const LeftSidebar = () => {
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-y-2">
|
||||
<SectionIcon id="basics" name="Basics" onClick={() => scrollIntoView("#basics")} />
|
||||
<SectionIcon
|
||||
id="basics"
|
||||
onClick={() => scrollIntoView("#basics")}
|
||||
name={t({
|
||||
message: "Basics",
|
||||
context:
|
||||
"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")} />
|
||||
@ -68,10 +77,11 @@ export const LeftSidebar = () => {
|
||||
<SectionIcon
|
||||
id="custom"
|
||||
variant="outline"
|
||||
name="Add a new section"
|
||||
name={t`Add a new section`}
|
||||
icon={<Plus size={14} />}
|
||||
onClick={() => {
|
||||
addSection();
|
||||
// eslint-disable-next-line lingui/no-unlocalized-strings
|
||||
scrollIntoView("& > section:last-of-type");
|
||||
}}
|
||||
/>
|
||||
@ -184,7 +194,7 @@ export const LeftSidebar = () => {
|
||||
|
||||
<Button size="lg" variant="outline" onClick={addSection}>
|
||||
<PlusCircle />
|
||||
<span className="ml-2">Add a new section</span>
|
||||
<span className="ml-2">{t`Add a new section`}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { basicsSchema } from "@reactive-resume/schema";
|
||||
import { Input, Label } from "@reactive-resume/ui";
|
||||
|
||||
@ -17,7 +18,7 @@ export const BasicsSection = () => {
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{getSectionIcon("basics")}
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">Basics</h2>
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">{t`Basics`}</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -27,10 +28,9 @@ export const BasicsSection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 sm:col-span-2">
|
||||
<Label htmlFor="basics.name">Full Name</Label>
|
||||
<Label htmlFor="basics.name">{t`Full Name`}</Label>
|
||||
<Input
|
||||
id="basics.name"
|
||||
placeholder="John Doe"
|
||||
value={basics.name}
|
||||
hasError={!basicsSchema.pick({ name: true }).safeParse({ name: basics.name }).success}
|
||||
onChange={(event) => setValue("basics.name", event.target.value)}
|
||||
@ -38,17 +38,16 @@ export const BasicsSection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 sm:col-span-2">
|
||||
<Label htmlFor="basics.headline">Headline</Label>
|
||||
<Label htmlFor="basics.headline">{t`Headline`}</Label>
|
||||
<Input
|
||||
id="basics.headline"
|
||||
placeholder="Highly Creative Frontend Web Developer"
|
||||
value={basics.headline}
|
||||
onChange={(event) => setValue("basics.headline", event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="basics.email">Email Address</Label>
|
||||
<Label htmlFor="basics.email">{t`Email`}</Label>
|
||||
<Input
|
||||
id="basics.email"
|
||||
placeholder="john.doe@example.com"
|
||||
@ -61,7 +60,7 @@ export const BasicsSection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="basics.url">Website</Label>
|
||||
<Label htmlFor="basics.url">{t`Website`}</Label>
|
||||
<URLInput
|
||||
id="basics.url"
|
||||
value={basics.url}
|
||||
@ -71,7 +70,7 @@ export const BasicsSection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="basics.phone">Phone Number</Label>
|
||||
<Label htmlFor="basics.phone">{t`Phone`}</Label>
|
||||
<Input
|
||||
id="basics.phone"
|
||||
placeholder="+1 (123) 4567 7890"
|
||||
@ -81,10 +80,9 @@ export const BasicsSection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="basics.location">Location</Label>
|
||||
<Label htmlFor="basics.location">{t`Location`}</Label>
|
||||
<Input
|
||||
id="basics.location"
|
||||
placeholder="105 Cedarhurst Ave, Cedarhurst, NY 11516"
|
||||
value={basics.location}
|
||||
onChange={(event) => setValue("basics.location", event.target.value)}
|
||||
/>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { DotsSixVertical, Plus, X } from "@phosphor-icons/react";
|
||||
import { CustomField as ICustomField } from "@reactive-resume/schema";
|
||||
@ -38,21 +39,22 @@ export const CustomField = ({ field, onChange, onRemove }: CustomFieldProps) =>
|
||||
<DotsSixVertical />
|
||||
</Button>
|
||||
|
||||
<Input
|
||||
placeholder="Icon"
|
||||
{/* <Input
|
||||
placeholder={t`Icon`}
|
||||
value={field.icon}
|
||||
className="!ml-0"
|
||||
onChange={(event) => handleChange("icon", event.target.value)}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Input
|
||||
placeholder="Name"
|
||||
placeholder={t`Name`}
|
||||
value={field.name}
|
||||
className="!ml-0"
|
||||
onChange={(event) => handleChange("name", event.target.value)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder="Value"
|
||||
placeholder={t`Value`}
|
||||
value={field.value}
|
||||
onChange={(event) => handleChange("value", event.target.value)}
|
||||
/>
|
||||
@ -126,7 +128,7 @@ export const CustomFieldsSection = ({ className }: Props) => {
|
||||
|
||||
<Button variant="link" onClick={onAddCustomField}>
|
||||
<Plus className="mr-2" />
|
||||
<span>Add a custom field</span>
|
||||
<span>{t`Add a custom field`}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import {
|
||||
AspectRatio,
|
||||
Checkbox,
|
||||
@ -69,7 +70,7 @@ export const PictureOptions = () => {
|
||||
<div className="flex flex-col gap-y-5">
|
||||
<div className="grid grid-cols-3 items-center gap-x-6">
|
||||
<Label htmlFor="picture.size" className="col-span-1">
|
||||
Size (in px)
|
||||
{t`Size (in px)`}
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
@ -85,7 +86,7 @@ export const PictureOptions = () => {
|
||||
|
||||
<div className="grid grid-cols-3 items-center gap-x-6">
|
||||
<Label htmlFor="picture.aspectRatio" className="col-span-1">
|
||||
Aspect Ratio
|
||||
{t`Aspect Ratio`}
|
||||
</Label>
|
||||
<div className="col-span-2 flex items-center justify-between">
|
||||
<ToggleGroup
|
||||
@ -94,19 +95,19 @@ export const PictureOptions = () => {
|
||||
onValueChange={onAspectRatioChange}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<Tooltip content="Square">
|
||||
<Tooltip content={t`Square`}>
|
||||
<ToggleGroupItem value="square">
|
||||
<div className="h-3 w-3 border border-foreground" />
|
||||
</ToggleGroupItem>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Horizontal">
|
||||
<Tooltip content={t`Horizontal`}>
|
||||
<ToggleGroupItem value="horizontal">
|
||||
<div className="h-2 w-3 border border-foreground" />
|
||||
</ToggleGroupItem>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Portrait">
|
||||
<Tooltip content={t`Portrait`}>
|
||||
<ToggleGroupItem value="portrait">
|
||||
<div className="h-3 w-2 border border-foreground" />
|
||||
</ToggleGroupItem>
|
||||
@ -130,7 +131,7 @@ export const PictureOptions = () => {
|
||||
|
||||
<div className="grid grid-cols-3 items-center gap-x-6">
|
||||
<Label htmlFor="picture.borderRadius" className="col-span-1">
|
||||
Border Radius
|
||||
{t`Border Radius`}
|
||||
</Label>
|
||||
<div className="col-span-2 flex items-center justify-between">
|
||||
<ToggleGroup
|
||||
@ -139,19 +140,19 @@ export const PictureOptions = () => {
|
||||
onValueChange={onBorderRadiusChange}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<Tooltip content="Square">
|
||||
<Tooltip content={t`Square`}>
|
||||
<ToggleGroupItem value="square">
|
||||
<div className="h-3 w-3 border border-foreground" />
|
||||
</ToggleGroupItem>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Rounded">
|
||||
<Tooltip content={t`Rounded`}>
|
||||
<ToggleGroupItem value="rounded">
|
||||
<div className="h-3 w-3 rounded-sm border border-foreground" />
|
||||
</ToggleGroupItem>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Circle">
|
||||
<Tooltip content={t`Circle`}>
|
||||
<ToggleGroupItem value="circle">
|
||||
<div className="h-3 w-3 rounded-full border border-foreground" />
|
||||
</ToggleGroupItem>
|
||||
@ -176,7 +177,7 @@ export const PictureOptions = () => {
|
||||
<div>
|
||||
<div className="grid grid-cols-3 items-start gap-x-6">
|
||||
<div className="col-span-1">
|
||||
<Label>Effects</Label>
|
||||
<Label>{t`Effects`}</Label>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
@ -187,7 +188,7 @@ export const PictureOptions = () => {
|
||||
setValue("basics.picture.effects.hidden", checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="picture.effects.hidden">Hidden</Label>
|
||||
<Label htmlFor="picture.effects.hidden">{t`Hidden`}</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
@ -198,7 +199,7 @@ export const PictureOptions = () => {
|
||||
setValue("basics.picture.effects.border", checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="picture.effects.border">Border</Label>
|
||||
<Label htmlFor="picture.effects.border">{t`Border`}</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
@ -209,7 +210,7 @@ export const PictureOptions = () => {
|
||||
setValue("basics.picture.effects.grayscale", checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="picture.effects.grayscale">Grayscale</Label>
|
||||
<Label htmlFor="picture.effects.grayscale">{t`Grayscale`}</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { Aperture, UploadSimple } from "@phosphor-icons/react";
|
||||
import {
|
||||
Avatar,
|
||||
@ -48,7 +49,7 @@ export const PictureSection = () => {
|
||||
</Avatar>
|
||||
|
||||
<div className="flex w-full flex-col gap-y-1.5">
|
||||
<Label htmlFor="basics.picture.url">Picture</Label>
|
||||
<Label htmlFor="basics.picture.url">{t`Picture`}</Label>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Input
|
||||
id="basics.picture.url"
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { t } from "@lingui/macro";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
import { SectionItem, SectionKey, SectionWithItem } from "@reactive-resume/schema";
|
||||
import { Button } from "@reactive-resume/ui";
|
||||
@ -103,7 +104,12 @@ export const SectionBase = <T extends SectionItem>({ id, title, description }: P
|
||||
className="gap-x-2 border-dashed py-6 leading-relaxed hover:bg-secondary-accent"
|
||||
>
|
||||
<Plus size={14} />
|
||||
<span className="font-medium">Add New {section.name}</span>
|
||||
<span className="font-medium">
|
||||
{t({
|
||||
message: "Add New Item",
|
||||
context: "For example, add a new work experience, or add a new profile.",
|
||||
})}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -137,7 +143,12 @@ export const SectionBase = <T extends SectionItem>({ id, title, description }: P
|
||||
<footer className="flex items-center justify-end">
|
||||
<Button variant="outline" className="ml-auto gap-x-2" onClick={onCreate}>
|
||||
<Plus />
|
||||
<span>Add New {section.name}</span>
|
||||
<span>
|
||||
{t({
|
||||
message: "Add New Item",
|
||||
context: "For example, add a new work experience, or add a new profile.",
|
||||
})}
|
||||
</span>
|
||||
</Button>
|
||||
</footer>
|
||||
)}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { CopySimple, PencilSimple, Plus } from "@phosphor-icons/react";
|
||||
import { SectionItem, SectionWithItem } from "@reactive-resume/schema";
|
||||
@ -20,7 +21,7 @@ import {
|
||||
} from "@reactive-resume/ui";
|
||||
import { produce } from "immer";
|
||||
import get from "lodash.get";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import { DialogName, useDialog } from "@/client/stores/dialog";
|
||||
@ -40,12 +41,12 @@ export const SectionDialog = <T extends SectionItem>({
|
||||
children,
|
||||
}: Props<T>) => {
|
||||
const { isOpen, mode, close, payload } = useDialog<T>(id);
|
||||
|
||||
const setValue = useResumeStore((state) => state.setValue);
|
||||
const section = useResumeStore((state) => {
|
||||
if (!id) return null;
|
||||
return get(state.resume.data.sections, id);
|
||||
}) as SectionWithItem<T> | null;
|
||||
const name = useMemo(() => section?.name ?? "", [section?.name]);
|
||||
|
||||
const isCreate = mode === "create";
|
||||
const isUpdate = mode === "update";
|
||||
@ -111,18 +112,16 @@ export const SectionDialog = <T extends SectionItem>({
|
||||
<Form {...form}>
|
||||
<form>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure you want to delete this {name}?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t`Are you sure you want to delete this item?`}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action can be reverted by clicking on the undo button in the floating
|
||||
toolbar.
|
||||
{t`This action can be reverted by clicking on the undo button in the floating toolbar.`}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<AlertDialogCancel>{t`Cancel`}</AlertDialogCancel>
|
||||
<AlertDialogAction variant="error" onClick={form.handleSubmit(onSubmit)}>
|
||||
Delete
|
||||
{t`Delete`}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
@ -144,9 +143,9 @@ export const SectionDialog = <T extends SectionItem>({
|
||||
{isUpdate && <PencilSimple />}
|
||||
{isDuplicate && <CopySimple />}
|
||||
<h2>
|
||||
{isCreate && `Create a new ${name}`}
|
||||
{isUpdate && `Update an existing ${name}`}
|
||||
{isDuplicate && `Duplicate an existing ${name}`}
|
||||
{isCreate && t`Create a new item`}
|
||||
{isUpdate && t`Update an existing item`}
|
||||
{isDuplicate && t`Duplicate an existing item`}
|
||||
</h2>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
@ -156,9 +155,9 @@ export const SectionDialog = <T extends SectionItem>({
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="submit">
|
||||
{isCreate && "Create"}
|
||||
{isUpdate && "Save Changes"}
|
||||
{isDuplicate && "Duplicate"}
|
||||
{isCreate && t`Create`}
|
||||
{isUpdate && t`Save Changes`}
|
||||
{isDuplicate && t`Duplicate`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { t } from "@lingui/macro";
|
||||
import { CopySimple, DotsSixVertical, PencilSimple, TrashSimple } from "@phosphor-icons/react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -81,19 +82,19 @@ export const SectionListItem = ({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center" side="left" sideOffset={-16}>
|
||||
<DropdownMenuCheckboxItem checked={visible} onCheckedChange={onToggleVisibility}>
|
||||
<span className="-ml-0.5">Visible</span>
|
||||
<span className="-ml-0.5">{t`Visible`}</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuItem onClick={onUpdate}>
|
||||
<PencilSimple size={14} />
|
||||
<span className="ml-2">Edit</span>
|
||||
<span className="ml-2">{t`Edit`}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onDuplicate}>
|
||||
<CopySimple size={14} />
|
||||
<span className="ml-2">Copy</span>
|
||||
<span className="ml-2">{t`Copy`}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-error" onClick={onDelete}>
|
||||
<TrashSimple size={14} />
|
||||
<span className="ml-2">Remove</span>
|
||||
<span className="ml-2">{t`Remove`}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { plural, t } from "@lingui/macro";
|
||||
import {
|
||||
ArrowCounterClockwise,
|
||||
Broom,
|
||||
@ -63,7 +64,7 @@ export const SectionOptions = ({ id }: Props) => {
|
||||
<>
|
||||
<DropdownMenuItem onClick={onCreate}>
|
||||
<Plus />
|
||||
<span className="ml-2">Add a new item</span>
|
||||
<span className="ml-2">{t`Add a new item`}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
@ -73,12 +74,12 @@ export const SectionOptions = ({ id }: Props) => {
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onClick={toggleVisibility}>
|
||||
{section.visible ? <Eye /> : <EyeSlash />}
|
||||
<span className="ml-2">{section.visible ? "Hide" : "Show"}</span>
|
||||
<span className="ml-2">{section.visible ? t`Hide` : t`Show`}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<PencilSimple />
|
||||
<span className="ml-2">Rename</span>
|
||||
<span className="ml-2">{t`Rename`}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<div className="relative col-span-2">
|
||||
@ -103,15 +104,15 @@ export const SectionOptions = ({ id }: Props) => {
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Columns />
|
||||
<span className="ml-2">Columns</span>
|
||||
<span className="ml-2">{t`Columns`}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup value={`${section.columns}`} onValueChange={onChangeColumns}>
|
||||
<DropdownMenuRadioItem value="1">1 Column</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="2">2 Columns</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="3">3 Columns</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="4">4 Columns</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="5">5 Columns</DropdownMenuRadioItem>
|
||||
{Array.from({ length: 5 }, (_, i) => i + 1).map((value) => (
|
||||
<DropdownMenuRadioItem value={`${value}`}>
|
||||
{plural(value, { one: "Column", other: "Columns" })}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
@ -119,12 +120,12 @@ export const SectionOptions = ({ id }: Props) => {
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem disabled={!hasItems} onClick={onResetItems}>
|
||||
<Broom />
|
||||
<span className="ml-2">Reset</span>
|
||||
<span className="ml-2">{t`Reset`}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-error" disabled={!isCustomSection} onClick={onRemove}>
|
||||
<TrashSimple />
|
||||
<span className="ml-2">Remove</span>
|
||||
<span className="ml-2">{t`Remove`}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { Tag } from "@phosphor-icons/react";
|
||||
import { URL, urlSchema } from "@reactive-resume/schema";
|
||||
import { Button, Input, Popover, PopoverContent, PopoverTrigger } from "@reactive-resume/ui";
|
||||
@ -36,14 +37,14 @@ export const URLInput = forwardRef<HTMLInputElement, Props>(
|
||||
<PopoverContent className="p-1.5">
|
||||
<Input
|
||||
value={value.label}
|
||||
placeholder="Label"
|
||||
placeholder={t`Label`}
|
||||
onChange={(event) => onChange({ ...value, label: event.target.value })}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{hasError && <small className="opacity-75">URL must start with https://</small>}
|
||||
{hasError && <small className="opacity-75">{t`URL must start with https://`}</small>}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { ScrollArea, Separator } from "@reactive-resume/ui";
|
||||
import { useRef } from "react";
|
||||
|
||||
@ -56,26 +57,30 @@ export const RightSidebar = () => {
|
||||
<div />
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-y-2">
|
||||
<SectionIcon id="template" name="Template" onClick={() => scrollIntoView("#template")} />
|
||||
<SectionIcon id="layout" name="Layout" onClick={() => scrollIntoView("#layout")} />
|
||||
<SectionIcon
|
||||
id="template"
|
||||
name={t`Template`}
|
||||
onClick={() => scrollIntoView("#template")}
|
||||
/>
|
||||
<SectionIcon id="layout" name={t`Layout`} onClick={() => scrollIntoView("#layout")} />
|
||||
<SectionIcon
|
||||
id="typography"
|
||||
name="Typography"
|
||||
name={t`Typography`}
|
||||
onClick={() => scrollIntoView("#typography")}
|
||||
/>
|
||||
<SectionIcon id="theme" name="Theme" onClick={() => scrollIntoView("#theme")} />
|
||||
<SectionIcon id="page" name="Page" onClick={() => scrollIntoView("#page")} />
|
||||
<SectionIcon id="sharing" name="Sharing" onClick={() => scrollIntoView("#sharing")} />
|
||||
<SectionIcon id="theme" name={t`Theme`} onClick={() => scrollIntoView("#theme")} />
|
||||
<SectionIcon id="page" name={t`Page`} onClick={() => scrollIntoView("#page")} />
|
||||
<SectionIcon id="sharing" name={t`Sharing`} onClick={() => scrollIntoView("#sharing")} />
|
||||
<SectionIcon
|
||||
id="statistics"
|
||||
name="Statistics"
|
||||
name={t`Statistics`}
|
||||
onClick={() => scrollIntoView("#statistics")}
|
||||
/>
|
||||
<SectionIcon id="export" name="Export" onClick={() => scrollIntoView("#export")} />
|
||||
<SectionIcon id="notes" name="Notes" onClick={() => scrollIntoView("#notes")} />
|
||||
<SectionIcon id="export" name={t`Export`} onClick={() => scrollIntoView("#export")} />
|
||||
<SectionIcon id="notes" name={t`Notes`} onClick={() => scrollIntoView("#notes")} />
|
||||
<SectionIcon
|
||||
id="information"
|
||||
name="Information"
|
||||
name={t`Information`}
|
||||
onClick={() => scrollIntoView("#information")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { CircleNotch, FileJs, FilePdf } from "@phosphor-icons/react";
|
||||
import { buttonVariants, Card, CardContent, CardDescription, CardTitle } from "@reactive-resume/ui";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
import { useToast } from "@/client/hooks/use-toast";
|
||||
import { usePrintResume } from "@/client/services/resume/print";
|
||||
import { useResumeStore } from "@/client/stores/resume";
|
||||
|
||||
import { getSectionIcon } from "../shared/section-icon";
|
||||
|
||||
export const ExportSection = () => {
|
||||
const { toast } = useToast();
|
||||
const { printResume, loading } = usePrintResume();
|
||||
|
||||
const onJsonExport = () => {
|
||||
@ -19,11 +18,6 @@ export const ExportSection = () => {
|
||||
const resumeJSON = JSON.stringify(resume.data, null, 2);
|
||||
|
||||
saveAs(new Blob([resumeJSON], { type: "application/json" }), filename);
|
||||
|
||||
toast({
|
||||
variant: "success",
|
||||
title: "A JSON snapshot of your resume has been successfully exported.",
|
||||
});
|
||||
};
|
||||
|
||||
const onPdfExport = async () => {
|
||||
@ -43,7 +37,7 @@ export const ExportSection = () => {
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{getSectionIcon("export")}
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">Export</h2>
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">{t`Export`}</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -57,10 +51,9 @@ export const ExportSection = () => {
|
||||
>
|
||||
<FileJs size={22} />
|
||||
<CardContent className="flex-1">
|
||||
<CardTitle className="text-sm">JSON</CardTitle>
|
||||
<CardTitle className="text-sm">{t`JSON`}</CardTitle>
|
||||
<CardDescription className="font-normal">
|
||||
Download a JSON snapshot of your resume. This file can be used to import your resume
|
||||
in the future, or can even be shared with others to collaborate.
|
||||
{t`Download a JSON snapshot of your resume. This file can be used to import your resume in the future, or can even be shared with others to collaborate.`}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -76,10 +69,9 @@ export const ExportSection = () => {
|
||||
{loading ? <CircleNotch size={22} className="animate-spin" /> : <FilePdf size={22} />}
|
||||
|
||||
<CardContent className="flex-1">
|
||||
<CardTitle className="text-sm">PDF</CardTitle>
|
||||
<CardTitle className="text-sm">{t`PDF`}</CardTitle>
|
||||
<CardDescription className="font-normal">
|
||||
Download a PDF of your resume. This file can be used to print your resume, send it to
|
||||
recruiters, or upload on job portals.
|
||||
{t`Download a PDF of your resume. This file can be used to print your resume, send it to recruiters, or upload on job portals.`}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { Book, EnvelopeSimpleOpen, GithubLogo, HandHeart } from "@phosphor-icons/react";
|
||||
import {
|
||||
buttonVariants,
|
||||
@ -14,28 +15,30 @@ import { getSectionIcon } from "../shared/section-icon";
|
||||
const DonateCard = () => (
|
||||
<Card className="space-y-4 bg-info text-info-foreground">
|
||||
<CardContent className="space-y-2">
|
||||
<CardTitle>Support the app by donating what you can!</CardTitle>
|
||||
<CardTitle>{t`Support the app by donating what you can!`}</CardTitle>
|
||||
<CardDescription className="space-y-2">
|
||||
<p>
|
||||
I built Reactive Resume mostly by myself during my spare time, with a lot of help from
|
||||
other great open-source contributors.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you like the app and want to support keeping it free forever, please donate whatever
|
||||
you can afford to give.
|
||||
</p>
|
||||
<Trans>
|
||||
<p>
|
||||
I built Reactive Resume mostly by myself during my spare time, with a lot of help from
|
||||
other great open-source contributors.
|
||||
</p>
|
||||
<p>
|
||||
If you like the app and want to support keeping it free forever, please donate whatever
|
||||
you can afford to give.
|
||||
</p>
|
||||
<p>Your donations could be tax-deductible, depending on your location.</p>
|
||||
</Trans>
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<a
|
||||
href="https://opencollective.com/reactive-resume"
|
||||
className={cn(buttonVariants({ size: "sm" }))}
|
||||
href="https://github.com/sponsors/AmruthPillai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<HandHeart size={14} weight="bold" className="mr-2" />
|
||||
<span>Donate to Reactive Resume</span>
|
||||
<span>{t`Donate to Reactive Resume`}</span>
|
||||
</a>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
@ -44,36 +47,37 @@ const DonateCard = () => (
|
||||
const IssuesCard = () => (
|
||||
<Card className="space-y-4">
|
||||
<CardContent className="space-y-2">
|
||||
<CardTitle>Found a bug, or have an idea for a new feature?</CardTitle>
|
||||
<CardTitle>{t`Found a bug, or have an idea for a new feature?`}</CardTitle>
|
||||
<CardDescription className="space-y-2">
|
||||
<p>I'm sure the app is not perfect, but I'd like for it to be.</p>
|
||||
|
||||
<p>
|
||||
If you faced any issues while creating your resume, or have an idea that would help you
|
||||
and other users in creating your resume more easily, drop an issue on the repository or
|
||||
send me an email about it.
|
||||
</p>
|
||||
<Trans>
|
||||
<p>I'm sure the app is not perfect, but I'd like for it to be.</p>
|
||||
<p>
|
||||
If you faced any issues while creating your resume, or have an idea that would help you
|
||||
and other users in creating your resume more easily, drop an issue on the repository or
|
||||
send me an email about it.
|
||||
</p>
|
||||
</Trans>
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter className="space-x-4">
|
||||
<a
|
||||
className={cn(buttonVariants({ size: "sm" }))}
|
||||
href="https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose"
|
||||
target="_blank"
|
||||
className={cn(buttonVariants({ size: "sm" }))}
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<GithubLogo size={14} weight="bold" className="mr-2" />
|
||||
<span>Raise an issue</span>
|
||||
<span>{t`Raise an issue`}</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
className={cn(buttonVariants({ size: "sm" }))}
|
||||
href="mailto:hello@amruthpillai.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<EnvelopeSimpleOpen size={14} weight="bold" className="mr-2" />
|
||||
<span>Send me a message</span>
|
||||
<span>{t`Send me a message`}</span>
|
||||
</a>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
@ -82,17 +86,18 @@ const IssuesCard = () => (
|
||||
const DocumentationCard = () => (
|
||||
<Card className="space-y-4">
|
||||
<CardContent className="space-y-2">
|
||||
<CardTitle>Don't know where to begin? Hit the docs!</CardTitle>
|
||||
<CardTitle>{t`Don't know where to begin? Hit the docs!`}</CardTitle>
|
||||
<CardDescription className="space-y-2">
|
||||
<p>
|
||||
The community has spent a lot of time writing the documentation for Reactive Resume, and
|
||||
I'm sure it will help you get started with the app.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
There are also a lot of examples to help you get started, and features that you might not
|
||||
know about which could help you build your perfect resume.
|
||||
</p>
|
||||
<Trans>
|
||||
<p>
|
||||
The community has spent a lot of time writing the documentation for Reactive Resume, and
|
||||
I'm sure it will help you get started with the app.
|
||||
</p>
|
||||
<p>
|
||||
There are also a lot of examples to help you get started, and features that you might
|
||||
not know about which could help you build your perfect resume.
|
||||
</p>
|
||||
</Trans>
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter className="space-x-4">
|
||||
@ -103,7 +108,7 @@ const DocumentationCard = () => (
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<Book size={14} weight="bold" className="mr-2" />
|
||||
<span>Documentation</span>
|
||||
<span>{t`Documentation`}</span>
|
||||
</a>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
@ -115,7 +120,7 @@ export const InformationSection = () => {
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{getSectionIcon("information")}
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">Information</h2>
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">{t`Information`}</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { ArrowCounterClockwise, DotsSixVertical, Plus, TrashSimple } from "@phosphor-icons/react";
|
||||
import { defaultMetadata } from "@reactive-resume/schema";
|
||||
import { Button, Portal, Tooltip } from "@reactive-resume/ui";
|
||||
@ -203,10 +204,10 @@ export const LayoutSection = () => {
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{getSectionIcon("layout")}
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">Layout</h2>
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">{t`Layout`}</h2>
|
||||
</div>
|
||||
|
||||
<Tooltip content="Reset Layout">
|
||||
<Tooltip content={t`Reset Layout`}>
|
||||
<Button size="icon" variant="ghost" onClick={onResetLayout}>
|
||||
<ArrowCounterClockwise />
|
||||
</Button>
|
||||
@ -232,7 +233,9 @@ export const LayoutSection = () => {
|
||||
return (
|
||||
<div key={pageIndex} className="rounded border p-3 pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="mb-3 text-xs font-bold">Page {pageIndex + 1}</p>
|
||||
<p className="mb-3 text-xs font-bold">
|
||||
<Trans>Page {pageIndex + 1}</Trans>
|
||||
</p>
|
||||
|
||||
{pageIndex !== 0 && (
|
||||
<Button
|
||||
@ -247,8 +250,8 @@ export const LayoutSection = () => {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 items-start gap-x-4">
|
||||
<Column id={mainIndex} name="Main" items={main} />
|
||||
<Column id={sidebarIndex} name="Sidebar" items={sidebar} />
|
||||
<Column id={mainIndex} name={t`Main`} items={main} />
|
||||
<Column id={sidebarIndex} name={t`Sidebar`} items={sidebar} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -261,7 +264,7 @@ export const LayoutSection = () => {
|
||||
|
||||
<Button variant="outline" className="ml-auto" onClick={onAddPage}>
|
||||
<Plus />
|
||||
<span className="ml-2">Add New Page</span>
|
||||
<span className="ml-2">{t`Add New Page`}</span>
|
||||
</Button>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { RichInput } from "@reactive-resume/ui";
|
||||
|
||||
import { useResumeStore } from "@/client/stores/resume";
|
||||
@ -13,22 +14,20 @@ export const NotesSection = () => {
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{getSectionIcon("notes")}
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">Notes</h2>
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">{t`Notes`}</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="grid gap-y-4">
|
||||
<p className="leading-relaxed">
|
||||
This section is reserved for your personal notes specific to this resume. The content here
|
||||
remains private and is not shared with anyone else.
|
||||
{t`This section is reserved for your personal notes specific to this resume. The content here remains private and is not shared with anyone else.`}
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<RichInput content={notes} onChange={(content) => setValue("metadata.notes", content)} />
|
||||
|
||||
<p className="text-xs leading-relaxed opacity-75">
|
||||
For example, information regarding which companies you sent this resume to or the links
|
||||
to the job descriptions can be noted down here.
|
||||
{t`For example, information regarding which companies you sent this resume to or the links to the job descriptions can be noted down here.`}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import {
|
||||
Label,
|
||||
Select,
|
||||
@ -22,13 +23,13 @@ export const PageSection = () => {
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{getSectionIcon("page")}
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">Page</h2>
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">{t`Page`}</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="grid gap-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Format</Label>
|
||||
<Label>{t`Format`}</Label>
|
||||
<Select
|
||||
value={page.format}
|
||||
onValueChange={(value) => {
|
||||
@ -36,17 +37,17 @@ export const PageSection = () => {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Format" />
|
||||
<SelectValue placeholder={t`Format`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="a4">A4</SelectItem>
|
||||
<SelectItem value="letter">Letter</SelectItem>
|
||||
<SelectItem value="a4">{t`A4`}</SelectItem>
|
||||
<SelectItem value="letter">{t`Letter`}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Margin</Label>
|
||||
<Label>{t`Margin`}</Label>
|
||||
<div className="flex items-center gap-x-4 py-1">
|
||||
<Slider
|
||||
min={0}
|
||||
@ -63,7 +64,7 @@ export const PageSection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Options</Label>
|
||||
<Label>{t`Options`}</Label>
|
||||
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-x-4">
|
||||
@ -74,7 +75,7 @@ export const PageSection = () => {
|
||||
setValue("metadata.page.options.breakLine", checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="metadata.page.options.breakLine">Show Break Line</Label>
|
||||
<Label htmlFor="metadata.page.options.breakLine">{t`Show Break Line`}</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -87,7 +88,7 @@ export const PageSection = () => {
|
||||
setValue("metadata.page.options.pageNumbers", checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="metadata.page.options.pageNumbers">Show Page Numbers</Label>
|
||||
<Label htmlFor="metadata.page.options.pageNumbers">{t`Show Page Numbers`}</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { CopySimple } from "@phosphor-icons/react";
|
||||
import { Button, Input, Label, Switch, Tooltip } from "@reactive-resume/ui";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
@ -25,9 +26,8 @@ export const SharingSection = () => {
|
||||
|
||||
toast({
|
||||
variant: "success",
|
||||
title: "A link has been copied to your clipboard.",
|
||||
description:
|
||||
"Anyone with this link can view and download the resume. Share it on your profile or with recruiters.",
|
||||
title: t`A link has been copied to your clipboard.`,
|
||||
description: t`Anyone with this link can view and download the resume. Share it on your profile or with recruiters.`,
|
||||
});
|
||||
};
|
||||
|
||||
@ -36,7 +36,7 @@ export const SharingSection = () => {
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{getSectionIcon("sharing")}
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">Sharing</h2>
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">{t`Sharing`}</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -52,9 +52,9 @@ export const SharingSection = () => {
|
||||
/>
|
||||
<div>
|
||||
<Label htmlFor="visibility" className="space-y-1">
|
||||
<p>Public</p>
|
||||
<p>{t`Public`}</p>
|
||||
<p className="text-xs opacity-60">
|
||||
Anyone with the link can view and download the resume.
|
||||
{t`Anyone with the link can view and download the resume.`}
|
||||
</p>
|
||||
</Label>
|
||||
</div>
|
||||
@ -70,12 +70,12 @@ export const SharingSection = () => {
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<Label htmlFor="resume-url">URL</Label>
|
||||
<Label htmlFor="resume-url">{t`URL`}</Label>
|
||||
|
||||
<div className="flex gap-x-1.5">
|
||||
<Input id="resume-url" readOnly value={url} className="flex-1" />
|
||||
|
||||
<Tooltip content="Copy to Clipboard">
|
||||
<Tooltip content={t`Copy to Clipboard`}>
|
||||
<Button size="icon" variant="ghost" onClick={onCopy}>
|
||||
<CopySimple />
|
||||
</Button>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { Info } from "@phosphor-icons/react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@reactive-resume/ui";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
@ -19,7 +20,7 @@ export const StatisticsSection = () => {
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{getSectionIcon("statistics")}
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">Statistics</h2>
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">{t`Statistics`}</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -34,12 +35,9 @@ export const StatisticsSection = () => {
|
||||
>
|
||||
<Alert variant="info">
|
||||
<Info size={18} />
|
||||
|
||||
<AlertTitle>Statistics are available only for public resumes.</AlertTitle>
|
||||
|
||||
<AlertTitle>{t`Statistics are available only for public resumes.`}</AlertTitle>
|
||||
<AlertDescription className="text-xs leading-relaxed">
|
||||
You can track the number of views your resume has received, or how many people
|
||||
have downloaded the resume by enabling public sharing.
|
||||
{t`You can track the number of views your resume has received, or how many people have downloaded the resume by enabling public sharing.`}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</motion.div>
|
||||
@ -50,14 +48,14 @@ export const StatisticsSection = () => {
|
||||
<h3 className={cn("text-4xl font-bold blur-none transition-all", !isPublic && "blur-sm")}>
|
||||
{statistics?.views ?? 0}
|
||||
</h3>
|
||||
<p className="opacity-75">Views</p>
|
||||
<p className="opacity-75">{t`Views`}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className={cn("text-4xl font-bold blur-none transition-all", !isPublic && "blur-sm")}>
|
||||
{statistics?.downloads ?? 0}
|
||||
</h3>
|
||||
<p className="opacity-75">Downloads</p>
|
||||
<p className="opacity-75">{t`Downloads`}</p>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { Button, HoverCard, HoverCardContent, HoverCardTrigger } from "@reactive-resume/ui";
|
||||
import { cn, templatesList } from "@reactive-resume/utils";
|
||||
|
||||
@ -14,7 +15,7 @@ export const TemplateSection = () => {
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{getSectionIcon("template")}
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">Template</h2>
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">{t`Template`}</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { Input, Label, Popover, PopoverContent, PopoverTrigger } from "@reactive-resume/ui";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
@ -16,7 +17,7 @@ export const ThemeSection = () => {
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{getSectionIcon("theme")}
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">Theme</h2>
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">{t`Theme`}</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -39,7 +40,7 @@ export const ThemeSection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="theme.primary">Primary Color</Label>
|
||||
<Label htmlFor="theme.primary">{t`Primary Color`}</Label>
|
||||
<div className="relative">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@ -69,7 +70,7 @@ export const ThemeSection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="theme.primary">Background Color</Label>
|
||||
<Label htmlFor="theme.primary">{t`Background Color`}</Label>
|
||||
<div className="relative">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@ -99,7 +100,7 @@ export const ThemeSection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="theme.primary">Text Color</Label>
|
||||
<Label htmlFor="theme.primary">{t`Text Color`}</Label>
|
||||
<div className="relative">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
/* eslint-disable lingui/no-unlocalized-strings */
|
||||
|
||||
import { t } from "@lingui/macro";
|
||||
import { Button, Combobox, ComboboxOption, Label, Slider, Switch } from "@reactive-resume/ui";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
import { fonts } from "@reactive-resume/utils";
|
||||
@ -61,7 +64,7 @@ export const TypographySection = () => {
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{getSectionIcon("typography")}
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">Typography</h2>
|
||||
<h2 className="line-clamp-1 text-3xl font-bold">{t`Typography`}</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -89,11 +92,11 @@ export const TypographySection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Font Family</Label>
|
||||
<Label>{t`Font Family`}</Label>
|
||||
<Combobox
|
||||
options={families}
|
||||
value={typography.font.family}
|
||||
searchPlaceholder="Search for a font family"
|
||||
searchPlaceholder={t`Search for a font family`}
|
||||
onValueChange={(value) => {
|
||||
setValue("metadata.typography.font.family", value);
|
||||
setValue("metadata.typography.font.subset", "latin");
|
||||
@ -104,11 +107,11 @@ export const TypographySection = () => {
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Font Subset</Label>
|
||||
<Label>{t`Font Subset`}</Label>
|
||||
<Combobox
|
||||
options={subsets}
|
||||
value={typography.font.subset}
|
||||
searchPlaceholder="Search for a font subset"
|
||||
searchPlaceholder={t`Search for a font subset`}
|
||||
onValueChange={(value) => {
|
||||
setValue("metadata.typography.font.subset", value);
|
||||
}}
|
||||
@ -116,12 +119,12 @@ export const TypographySection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Font Variants</Label>
|
||||
<Label>{t`Font Variants`}</Label>
|
||||
<Combobox
|
||||
multiple
|
||||
options={variants}
|
||||
value={typography.font.variants}
|
||||
searchPlaceholder="Search for a font variant"
|
||||
searchPlaceholder={t`Search for a font variant`}
|
||||
onValueChange={(value) => {
|
||||
setValue("metadata.typography.font.variants", value);
|
||||
}}
|
||||
@ -130,7 +133,7 @@ export const TypographySection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Font Size</Label>
|
||||
<Label>{t`Font Size`}</Label>
|
||||
<div className="flex items-center gap-x-4 py-1">
|
||||
<Slider
|
||||
min={12}
|
||||
@ -147,7 +150,7 @@ export const TypographySection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Line Height</Label>
|
||||
<Label>{t`Line Height`}</Label>
|
||||
<div className="flex items-center gap-x-4 py-1">
|
||||
<Slider
|
||||
min={0}
|
||||
@ -164,7 +167,7 @@ export const TypographySection = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Options</Label>
|
||||
<Label>{t`Options`}</Label>
|
||||
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-x-4">
|
||||
@ -175,7 +178,7 @@ export const TypographySection = () => {
|
||||
setValue("metadata.typography.underlineLinks", checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="metadata.typography.underlineLinks">Underline Links</Label>
|
||||
<Label htmlFor="metadata.typography.underlineLinks">{t`Underline Links`}</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { FadersHorizontal, ReadCvLogo } from "@phosphor-icons/react";
|
||||
import { Button, KeyboardShortcut, Separator } from "@reactive-resume/ui";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
@ -33,21 +34,6 @@ interface SidebarItem {
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
const sidebarItems: SidebarItem[] = [
|
||||
{
|
||||
path: "/dashboard/resumes",
|
||||
name: "Resumes",
|
||||
shortcut: "⇧R",
|
||||
icon: <ReadCvLogo />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/settings",
|
||||
name: "Settings",
|
||||
shortcut: "⇧S",
|
||||
icon: <FadersHorizontal />,
|
||||
},
|
||||
];
|
||||
|
||||
type SidebarItemProps = SidebarItem & {
|
||||
onClick?: () => void;
|
||||
};
|
||||
@ -94,6 +80,21 @@ export const Sidebar = ({ setOpen }: SidebarProps) => {
|
||||
setOpen?.(false);
|
||||
});
|
||||
|
||||
const sidebarItems: SidebarItem[] = [
|
||||
{
|
||||
path: "/dashboard/resumes",
|
||||
name: t`Resumes`,
|
||||
shortcut: "⇧R",
|
||||
icon: <ReadCvLogo />,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/settings",
|
||||
name: t`Settings`,
|
||||
shortcut: "⇧S",
|
||||
icon: <FadersHorizontal />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-y-4">
|
||||
<div className="ml-12 flex justify-center lg:ml-0">
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { Check, DownloadSimple, Warning } from "@phosphor-icons/react";
|
||||
import {
|
||||
JsonResume,
|
||||
@ -141,7 +142,7 @@ export const ImportDialog = () => {
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while validating the file.",
|
||||
title: t`An error occurred while validating the file.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -186,7 +187,7 @@ export const ImportDialog = () => {
|
||||
toast({
|
||||
variant: "error",
|
||||
icon: <Warning size={16} weight="bold" />,
|
||||
title: "An error occurred while importing your resume.",
|
||||
title: t`An error occurred while importing your resume.`,
|
||||
description: importError?.message,
|
||||
});
|
||||
}
|
||||
@ -206,12 +207,11 @@ export const ImportDialog = () => {
|
||||
<DialogTitle>
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<DownloadSimple />
|
||||
<h2>Import an existing resume</h2>
|
||||
<h2>{t`Import an existing resume`}</h2>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Upload a file from an external source to parse an existing resume and import it into
|
||||
Reactive Resume for easier editing.
|
||||
{t`Upload a file from one of the accepted sources to parse existing data and import it into Reactive Resume for easier editing.`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@ -220,20 +220,24 @@ export const ImportDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Type</FormLabel>
|
||||
<FormLabel>{t`Filetype`}</FormLabel>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Please select a file type" />
|
||||
<SelectValue placeholder={t`Please select a file type`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||
<SelectItem value="reactive-resume-json">
|
||||
Reactive Resume (.json)
|
||||
</SelectItem>
|
||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||
<SelectItem value="reactive-resume-v3-json">
|
||||
Reactive Resume v3 (.json)
|
||||
</SelectItem>
|
||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||
<SelectItem value="json-resume-json">JSON Resume (.json)</SelectItem>
|
||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||
<SelectItem value="linkedin-data-export-zip">
|
||||
LinkedIn Data Export (.zip)
|
||||
</SelectItem>
|
||||
@ -250,7 +254,7 @@ export const ImportDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>File</FormLabel>
|
||||
<FormLabel>{t`File`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="file"
|
||||
@ -263,14 +267,22 @@ export const ImportDialog = () => {
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
{accept && <FormDescription>Accepts only {accept} files</FormDescription>}
|
||||
{accept && (
|
||||
<FormDescription>
|
||||
{t({
|
||||
message: `Accepts only ${accept} files`,
|
||||
comment:
|
||||
"Helper text to let the user know what filetypes are accepted. {accept} can be .pdf or .json.",
|
||||
})}
|
||||
</FormDescription>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{validationResult?.isValid === false && validationResult.errors !== undefined && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-error">Errors during Validation</Label>
|
||||
<Label className="text-error">{t`Errors`}</Label>
|
||||
<ScrollArea orientation="vertical" className="h-[180px]">
|
||||
<div className="whitespace-pre-wrap rounded bg-secondary-accent p-4 font-mono text-xs leading-relaxed">
|
||||
{JSON.stringify(validationResult.errors, null, 4)}
|
||||
@ -283,25 +295,25 @@ export const ImportDialog = () => {
|
||||
<AnimatePresence presenceAffectsLayout>
|
||||
{(!validationResult ?? false) && (
|
||||
<Button type="button" onClick={onValidate}>
|
||||
Validate
|
||||
{t`Validate`}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{validationResult !== null && !validationResult.isValid && (
|
||||
<Button type="button" variant="secondary" onClick={onReset}>
|
||||
Reset
|
||||
{t`Discard`}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{validationResult !== null && validationResult.isValid && (
|
||||
<>
|
||||
<Button type="button" onClick={onImport} disabled={loading}>
|
||||
Import
|
||||
{t`Import`}
|
||||
</Button>
|
||||
|
||||
<Button disabled type="button" variant="success">
|
||||
<Check size={16} weight="bold" className="mr-2" />
|
||||
Validated
|
||||
{t`Validated`}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { ResumeDto } from "@reactive-resume/dto";
|
||||
import {
|
||||
AlertDialog,
|
||||
@ -34,22 +35,21 @@ export const LockDialog = () => {
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{isLockMode && "Are you sure you want to lock this resume?"}
|
||||
{isUnlockMode && "Are you sure you want to unlock this resume?"}
|
||||
{isLockMode && t`Are you sure you want to lock this resume?`}
|
||||
{isUnlockMode && t`Are you sure you want to unlock this resume?`}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{isLockMode &&
|
||||
"Locking a resume will prevent any further changes to it. This is useful when you have already shared your resume with someone and you don't want to accidentally make any changes to it."}
|
||||
{isUnlockMode && "Unlocking a resume will allow you to make changes to it again."}
|
||||
t`Locking a resume will prevent any further changes to it. This is useful when you have already shared your resume with someone and you don't want to accidentally make any changes to it.`}
|
||||
{isUnlockMode && t`Unlocking a resume will allow you to make changes to it again.`}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<AlertDialogCancel>{t`Cancel`}</AlertDialogCancel>
|
||||
<AlertDialogAction variant="info" disabled={loading} onClick={onSubmit}>
|
||||
{isLockMode && "Lock"}
|
||||
{isUnlockMode && "Unlock"}
|
||||
{isLockMode && t`Lock`}
|
||||
{isUnlockMode && t`Unlock`}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { CaretDown, Flask, MagicWand, Plus } from "@phosphor-icons/react";
|
||||
import { createResumeSchema, ResumeDto } from "@reactive-resume/dto";
|
||||
import { idSchema, sampleResume } from "@reactive-resume/schema";
|
||||
@ -116,7 +117,7 @@ export const ResumeDialog = () => {
|
||||
|
||||
toast({
|
||||
variant: "error",
|
||||
title: "An error occurred while trying process your request.",
|
||||
title: t`An error occurred while trying to create your resume.`,
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
@ -159,18 +160,16 @@ export const ResumeDialog = () => {
|
||||
<Form {...form}>
|
||||
<form>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure you want to delete your resume?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t`Are you sure you want to delete your resume?`}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your resume and cannot
|
||||
be recovered.
|
||||
{t`This action cannot be undone. This will permanently delete your resume and cannot be recovered.`}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<AlertDialogCancel>{t`Cancel`}</AlertDialogCancel>
|
||||
<AlertDialogAction variant="error" onClick={form.handleSubmit(onSubmit)}>
|
||||
Delete
|
||||
{t`Delete`}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
@ -190,16 +189,16 @@ export const ResumeDialog = () => {
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<Plus />
|
||||
<h2>
|
||||
{isCreate && "Create a new resume"}
|
||||
{isUpdate && "Update an existing resume"}
|
||||
{isDuplicate && "Duplicate an existing resume"}
|
||||
{isCreate && t`Create a new resume`}
|
||||
{isUpdate && t`Update an existing resume`}
|
||||
{isDuplicate && t`Duplicate an existing resume`}
|
||||
</h2>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isCreate && "Start building your resume by giving it a name."}
|
||||
{isUpdate && "Changed your mind about the name? Give it a new one."}
|
||||
{isDuplicate && "Give your old resume a new name."}
|
||||
{isCreate && t`Start building your resume by giving it a name.`}
|
||||
{isUpdate && t`Changed your mind about the name? Give it a new one.`}
|
||||
{isDuplicate && t`Give your old resume a new name.`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@ -208,13 +207,13 @@ export const ResumeDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormLabel>{t`Title`}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<Input {...field} className="flex-1" />
|
||||
|
||||
{(isCreate || isDuplicate) && (
|
||||
<Tooltip content="Generate a random name">
|
||||
<Tooltip content={t`Generate a random title for your resume`}>
|
||||
<Button
|
||||
size="icon"
|
||||
type="button"
|
||||
@ -228,7 +227,7 @@ export const ResumeDialog = () => {
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Tip: You can name the resume referring to the position you are applying for.
|
||||
{t`Tip: You can name the resume referring to the position you are applying for.`}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -240,7 +239,7 @@ export const ResumeDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Slug</FormLabel>
|
||||
<FormLabel>{t`Slug`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@ -252,9 +251,9 @@ export const ResumeDialog = () => {
|
||||
<DialogFooter>
|
||||
<div className="flex items-center">
|
||||
<Button type="submit" disabled={loading} className="rounded-r-none">
|
||||
{isCreate && "Create"}
|
||||
{isUpdate && "Save Changes"}
|
||||
{isDuplicate && "Duplicate"}
|
||||
{isCreate && t`Create`}
|
||||
{isUpdate && t`Save Changes`}
|
||||
{isDuplicate && t`Duplicate`}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@ -265,7 +264,7 @@ export const ResumeDialog = () => {
|
||||
<DropdownMenuContent side="right" align="center">
|
||||
<DropdownMenuItem onClick={onCreateSample}>
|
||||
<Flask className="mr-2" />
|
||||
Create Sample Resume
|
||||
{t`Create Sample Resume`}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
import { KeyboardShortcut } from "@reactive-resume/ui";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
@ -20,11 +21,12 @@ export const CreateResumeCard = () => {
|
||||
)}
|
||||
>
|
||||
<h4 className="font-medium">
|
||||
Create a new resume
|
||||
<KeyboardShortcut className="ml-2">(^N)</KeyboardShortcut>
|
||||
{t`Create a new resume`}
|
||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||
<KeyboardShortcut className="ml-2">^N</KeyboardShortcut>
|
||||
</h4>
|
||||
|
||||
<p className="text-xs opacity-75">Start from scratch</p>
|
||||
<p className="text-xs opacity-75">{t`Start from scratch`}</p>
|
||||
</div>
|
||||
</BaseCard>
|
||||
);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { DownloadSimple } from "@phosphor-icons/react";
|
||||
import { KeyboardShortcut } from "@reactive-resume/ui";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
@ -20,11 +21,12 @@ export const ImportResumeCard = () => {
|
||||
)}
|
||||
>
|
||||
<h4 className="line-clamp-1 font-medium">
|
||||
Import an existing resume
|
||||
<KeyboardShortcut className="ml-2">(^I)</KeyboardShortcut>
|
||||
{t`Import an existing resume`}
|
||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||
<KeyboardShortcut className="ml-2">^I</KeyboardShortcut>
|
||||
</h4>
|
||||
|
||||
<p className="line-clamp-1 text-xs opacity-75">LinkedIn, JSON Resume, etc.</p>
|
||||
<p className="line-clamp-1 text-xs opacity-75">{t`LinkedIn, JSON Resume, etc.`}</p>
|
||||
</div>
|
||||
</BaseCard>
|
||||
);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import {
|
||||
CircleNotch,
|
||||
CopySimple,
|
||||
@ -112,7 +113,7 @@ export const ResumeCard = ({ resume }: Props) => {
|
||||
)}
|
||||
>
|
||||
<h4 className="line-clamp-2 font-medium">{resume.title}</h4>
|
||||
<p className="line-clamp-1 text-xs opacity-75">{`Last updated ${lastUpdated}`}</p>
|
||||
<p className="line-clamp-1 text-xs opacity-75">{t`Last updated ${lastUpdated}`}</p>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</ContextMenuTrigger>
|
||||
@ -120,31 +121,31 @@ export const ResumeCard = ({ resume }: Props) => {
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem onClick={onOpen}>
|
||||
<FolderOpen size={14} className="mr-2" />
|
||||
Open
|
||||
{t`Open`}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={onUpdate}>
|
||||
<PencilSimple size={14} className="mr-2" />
|
||||
Rename
|
||||
{t`Rename`}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={onDuplicate}>
|
||||
<CopySimple size={14} className="mr-2" />
|
||||
Duplicate
|
||||
{t`Duplicate`}
|
||||
</ContextMenuItem>
|
||||
{resume.locked ? (
|
||||
<ContextMenuItem onClick={onLockChange}>
|
||||
<LockOpen size={14} className="mr-2" />
|
||||
Unlock
|
||||
{t`Unlock`}
|
||||
</ContextMenuItem>
|
||||
) : (
|
||||
<ContextMenuItem onClick={onLockChange}>
|
||||
<Lock size={14} className="mr-2" />
|
||||
Lock
|
||||
{t`Lock`}
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onClick={onDelete} className="text-error">
|
||||
<TrashSimple size={14} className="mr-2" />
|
||||
Delete
|
||||
{t`Delete`}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
import { ResumeDto } from "@reactive-resume/dto";
|
||||
import { KeyboardShortcut } from "@reactive-resume/ui";
|
||||
@ -15,11 +16,12 @@ export const CreateResumeListItem = () => {
|
||||
onClick={() => open("create")}
|
||||
title={
|
||||
<>
|
||||
<span>Create a new resume</span>
|
||||
<KeyboardShortcut className="ml-2">(^N)</KeyboardShortcut>
|
||||
<span>{t`Create a new resume`}</span>
|
||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||
<KeyboardShortcut className="ml-2">^N</KeyboardShortcut>
|
||||
</>
|
||||
}
|
||||
description="Start building from scratch"
|
||||
description={t`Start building from scratch`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { DownloadSimple } from "@phosphor-icons/react";
|
||||
import { KeyboardShortcut } from "@reactive-resume/ui";
|
||||
|
||||
@ -14,11 +15,12 @@ export const ImportResumeListItem = () => {
|
||||
onClick={() => open("create")}
|
||||
title={
|
||||
<>
|
||||
<span>Import an existing resume</span>
|
||||
<KeyboardShortcut className="ml-2">(^I)</KeyboardShortcut>
|
||||
<span>{t`Import an existing resume`}</span>
|
||||
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
|
||||
<KeyboardShortcut className="ml-2">^I</KeyboardShortcut>
|
||||
</>
|
||||
}
|
||||
description="LinkedIn, JSON Resume, etc."
|
||||
description={t`LinkedIn, JSON Resume, etc.`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import {
|
||||
CopySimple,
|
||||
DotsThreeVertical,
|
||||
@ -73,7 +74,7 @@ export const ResumeListItem = ({ resume }: Props) => {
|
||||
}}
|
||||
>
|
||||
<FolderOpen size={14} className="mr-2" />
|
||||
Open
|
||||
{t`Open`}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(event) => {
|
||||
@ -82,7 +83,7 @@ export const ResumeListItem = ({ resume }: Props) => {
|
||||
}}
|
||||
>
|
||||
<PencilSimple size={14} className="mr-2" />
|
||||
Rename
|
||||
{t`Rename`}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(event) => {
|
||||
@ -91,7 +92,7 @@ export const ResumeListItem = ({ resume }: Props) => {
|
||||
}}
|
||||
>
|
||||
<CopySimple size={14} className="mr-2" />
|
||||
Duplicate
|
||||
{t`Duplicate`}
|
||||
</DropdownMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
@ -102,7 +103,7 @@ export const ResumeListItem = ({ resume }: Props) => {
|
||||
}}
|
||||
>
|
||||
<TrashSimple size={14} className="mr-2" />
|
||||
Delete
|
||||
{t`Delete`}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@ -117,7 +118,7 @@ export const ResumeListItem = ({ resume }: Props) => {
|
||||
onClick={onOpen}
|
||||
className="group"
|
||||
title={resume.title}
|
||||
description={`Last updated ${lastUpdated}`}
|
||||
description={t`Last updated ${lastUpdated}`}
|
||||
end={dropdownMenu}
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
@ -142,20 +143,20 @@ export const ResumeListItem = ({ resume }: Props) => {
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem onClick={onOpen}>
|
||||
<FolderOpen size={14} className="mr-2" />
|
||||
Open
|
||||
{t`Open`}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={onUpdate}>
|
||||
<PencilSimple size={14} className="mr-2" />
|
||||
Rename
|
||||
{t`Rename`}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={onDuplicate}>
|
||||
<CopySimple size={14} className="mr-2" />
|
||||
Duplicate
|
||||
{t`Duplicate`}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onClick={onDelete} className="text-error">
|
||||
<TrashSimple size={14} className="mr-2" />
|
||||
Delete
|
||||
{t`Delete`}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { List, SquaresFour } from "@phosphor-icons/react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@reactive-resume/ui";
|
||||
import { motion } from "framer-motion";
|
||||
@ -15,7 +16,9 @@ export const ResumesPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Resumes - Reactive Resume</title>
|
||||
<title>
|
||||
{t`Resumes`} - {t`Reactive Resume`}
|
||||
</title>
|
||||
</Helmet>
|
||||
|
||||
<Tabs value={layout} onValueChange={(value) => setLayout(value as Layout)}>
|
||||
@ -25,17 +28,17 @@ export const ResumesPage = () => {
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="text-4xl font-bold tracking-tight"
|
||||
>
|
||||
Resumes
|
||||
{t`Resumes`}
|
||||
</motion.h1>
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value="grid" className="h-8 w-8 p-0 sm:h-8 sm:w-auto sm:px-4">
|
||||
<SquaresFour />
|
||||
<span className="ml-2 hidden sm:block">Grid</span>
|
||||
<span className="ml-2 hidden sm:block">{t`Grid`}</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="list" className="h-8 w-8 p-0 sm:h-8 sm:w-auto sm:px-4">
|
||||
<List />
|
||||
<span className="ml-2 hidden sm:block">List</span>
|
||||
<span className="ml-2 hidden sm:block">{t`List`}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { QrCode } from "@phosphor-icons/react";
|
||||
import {
|
||||
Alert,
|
||||
@ -46,7 +47,8 @@ import { useDialog } from "@/client/stores/dialog";
|
||||
|
||||
const formSchema = z.object({
|
||||
uri: z.literal("").or(z.string().optional()),
|
||||
code: z.literal("").or(z.string().regex(/^\d{6}$/, "Code must be exactly 6 digits long.")),
|
||||
// eslint-disable-next-line lingui/t-call-in-function
|
||||
code: z.literal("").or(z.string().regex(/^\d{6}$/, t`Code must be exactly 6 digits long.`)),
|
||||
backupCodes: z.array(z.string()),
|
||||
});
|
||||
|
||||
@ -103,7 +105,7 @@ export const TwoFactorDialog = () => {
|
||||
|
||||
toast({
|
||||
variant: "error",
|
||||
title: "An error occurred while trying to enable two-factor authentication.",
|
||||
title: t`An error occurred while trying to enable two-factor authentication.`,
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
@ -131,23 +133,21 @@ export const TwoFactorDialog = () => {
|
||||
<form className="space-y-4">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure you want to disable two-factor authentication?
|
||||
{t`Are you sure you want to disable two-factor authentication?`}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
If you disable two-factor authentication, you will no longer be required to enter
|
||||
a verification code when logging in.
|
||||
{t`If you disable two-factor authentication, you will no longer be required to enter a verification code when logging in.`}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<Alert variant="info">
|
||||
<AlertDescription>Note: This will make your account less secure.</AlertDescription>
|
||||
<AlertDescription>{t`Note: This will make your account less secure.`}</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<AlertDialogCancel>{t`Cancel`}</AlertDialogCancel>
|
||||
<AlertDialogAction variant="error" onClick={form.handleSubmit(onSubmit)}>
|
||||
Disable
|
||||
{t`Disable`}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
@ -167,19 +167,19 @@ export const TwoFactorDialog = () => {
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<QrCode />
|
||||
<h2>
|
||||
{mode === "create" && "Setup two-factor authentication on your account"}
|
||||
{mode === "create" && t`Setup two-factor authentication on your account`}
|
||||
{mode === "update" &&
|
||||
"Verify that two-factor authentication has been setup correctly"}
|
||||
{mode === "duplicate" && "Store your backup codes securely"}
|
||||
t`Verify that two-factor authentication has been setup correctly`}
|
||||
{mode === "duplicate" && t`Store your backup codes securely`}
|
||||
</h2>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isCreate &&
|
||||
"Scan the QR code below with your authenticator app to setup 2FA on your account."}
|
||||
t`Scan the QR code below with your authenticator app to setup 2FA on your account.`}
|
||||
{isUpdate &&
|
||||
"Enter the 6-digit code from your authenticator app to verify that 2FA has been setup correctly."}
|
||||
{isDuplicate && "You have enabled two-factor authentication successfully."}
|
||||
t`Enter the 6-digit code from your authenticator app to verify that 2FA has been setup correctly.`}
|
||||
{isDuplicate && t`You have enabled two-factor authentication successfully.`}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@ -196,8 +196,7 @@ export const TwoFactorDialog = () => {
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
In case you don't have access to your camera, you can also copy-paste this URI
|
||||
to your authenticator app.
|
||||
{t`In case you are unable to scan this QR Code, you can also copy-paste this link into your authenticator app.`}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -210,7 +209,7 @@ export const TwoFactorDialog = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Code</FormLabel>
|
||||
<FormLabel>{t`Code`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="123456" {...field} />
|
||||
</FormControl>
|
||||
@ -237,24 +236,23 @@ export const TwoFactorDialog = () => {
|
||||
/>
|
||||
|
||||
<p className="text-xs leading-relaxed">
|
||||
Please store your backup codes in a secure location. You can use one of these
|
||||
one-time use codes to login in case you lose access to your authenticator app.
|
||||
{t`Please store your backup codes in a secure location. You can use one of these one-time use codes to login in case you lose access to your authenticator app.`}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
{isCreate && <Button disabled={loading}>Continue</Button>}
|
||||
{isCreate && <Button disabled={loading}>{t`Continue`}</Button>}
|
||||
{isUpdate && (
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => open("create")}>
|
||||
Back
|
||||
{t`Back`}
|
||||
</Button>
|
||||
|
||||
<Button disabled={loading}>Continue</Button>
|
||||
<Button disabled={loading}>{t`Continue`}</Button>
|
||||
</>
|
||||
)}
|
||||
{isDuplicate && <Button disabled={loading}>Close</Button>}
|
||||
{isDuplicate && <Button disabled={loading}>{t`Close`}</Button>}
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t } from "@lingui/macro";
|
||||
import { Check, UploadSimple, Warning } from "@phosphor-icons/react";
|
||||
import { UpdateUserDto, updateUserSchema } from "@reactive-resume/dto";
|
||||
import {
|
||||
@ -65,7 +66,7 @@ export const AccountSettings = () => {
|
||||
if (user.email !== data.email) {
|
||||
toast({
|
||||
variant: "info",
|
||||
title: "Check your email for the confirmation link to update your email address.",
|
||||
title: t`Check your email for the confirmation link to update your email address.`,
|
||||
});
|
||||
}
|
||||
|
||||
@ -100,10 +101,9 @@ export const AccountSettings = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">Account</h3>
|
||||
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`Account`}</h3>
|
||||
<p className="leading-relaxed opacity-75">
|
||||
Here, you can update your account information such as your profile picture, name and
|
||||
username.
|
||||
{t`Here, you can update your account information such as your profile picture, name and username.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -117,7 +117,7 @@ export const AccountSettings = () => {
|
||||
<UserAvatar />
|
||||
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>Picture</FormLabel>
|
||||
<FormLabel>{t`Picture`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://..." {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
@ -149,7 +149,7 @@ export const AccountSettings = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t`Name`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@ -162,7 +162,7 @@ export const AccountSettings = () => {
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormLabel>{t`Username`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@ -180,7 +180,7 @@ export const AccountSettings = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>{t`Email`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" {...field} />
|
||||
</FormControl>
|
||||
@ -191,14 +191,14 @@ export const AccountSettings = () => {
|
||||
)}
|
||||
>
|
||||
{user.emailVerified ? <Check size={12} /> : <Warning size={12} />}
|
||||
{user.emailVerified ? "Verified" : "Unverified"}
|
||||
{user.emailVerified ? t`Verified` : t`Unverified`}
|
||||
{!user.emailVerified && (
|
||||
<Button
|
||||
variant="link"
|
||||
className="h-auto text-xs"
|
||||
onClick={onResendVerificationEmail}
|
||||
>
|
||||
Resend confirmation link
|
||||
{t`Resend email confirmation link`}
|
||||
</Button>
|
||||
)}
|
||||
</FormDescription>
|
||||
@ -216,10 +216,10 @@ export const AccountSettings = () => {
|
||||
className="flex items-center space-x-2 self-center sm:col-start-2"
|
||||
>
|
||||
<Button type="submit" disabled={loading}>
|
||||
Save Changes
|
||||
{t`Save Changes`}
|
||||
</Button>
|
||||
<Button type="reset" variant="ghost" onClick={onReset}>
|
||||
Reset
|
||||
{t`Discard`}
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
@ -48,7 +49,7 @@ export const DangerZoneSettings = () => {
|
||||
|
||||
toast({
|
||||
variant: "success",
|
||||
title: "Your account has been deleted successfully.",
|
||||
title: t`Your account and all your data has been deleted successfully. Goodbye!`,
|
||||
});
|
||||
|
||||
navigate("/");
|
||||
@ -58,11 +59,13 @@ export const DangerZoneSettings = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">Danger Zone</h3>
|
||||
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`Danger Zone`}</h3>
|
||||
<p className="leading-relaxed opacity-75">
|
||||
In this section, you can delete your account and all the data associated to your user, but
|
||||
please keep in mind that{" "}
|
||||
<span className="font-semibold">this action is irreversible</span>.
|
||||
<Trans>
|
||||
In this section, you can delete your account and all the data associated to your user,
|
||||
but please keep in mind that{" "}
|
||||
<span className="font-semibold">this action is irreversible</span>.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -73,12 +76,14 @@ export const DangerZoneSettings = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Delete Account</FormLabel>
|
||||
<FormLabel>{t`Delete Account`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="delete" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Type <code className="font-bold">delete</code> to confirm deleting your account.
|
||||
<Trans>
|
||||
Type <code className="font-bold">delete</code> to confirm deleting your account.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@ -86,7 +91,7 @@ export const DangerZoneSettings = () => {
|
||||
|
||||
<div className="flex items-center space-x-2 self-center">
|
||||
<Button type="submit" variant="error" disabled={!form.formState.isValid || loading}>
|
||||
{count === 1 ? "Are you sure?" : "Delete Account"}
|
||||
{count === 1 ? t`Are you sure?` : t`Delete Account`}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { LockSimple, LockSimpleOpen, TrashSimple } from "@phosphor-icons/react";
|
||||
import {
|
||||
Alert,
|
||||
@ -20,7 +21,8 @@ import { useOpenAiStore } from "@/client/stores/openai";
|
||||
const formSchema = z.object({
|
||||
apiKey: z
|
||||
.string()
|
||||
.regex(/^sk-[a-zA-Z0-9]+$/, "That doesn't look like a valid OpenAI API key.")
|
||||
// eslint-disable-next-line lingui/t-call-in-function
|
||||
.regex(/^sk-[a-zA-Z0-9]+$/, t`That doesn't look like a valid OpenAI API key.`)
|
||||
.default(""),
|
||||
});
|
||||
|
||||
@ -47,26 +49,27 @@ export const OpenAISettings = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">OpenAI Integration</h3>
|
||||
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`OpenAI Integration`}</h3>
|
||||
<p className="leading-relaxed opacity-75">
|
||||
You can make use of the OpenAI API to help you generate content, or improve your writing
|
||||
while composing your resume.
|
||||
{t`You can make use of the OpenAI API to help you generate content, or improve your writing while composing your resume.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-sm prose-zinc max-w-full dark:prose-invert">
|
||||
<p>
|
||||
You have the option to{" "}
|
||||
<a
|
||||
href="https://www.howtogeek.com/885918/how-to-get-an-openai-api-key/"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
obtain your own OpenAI API key
|
||||
</a>
|
||||
. This key empowers you to leverage the API as you see fit. Alternatively, if you wish to
|
||||
disable the AI features in Reactive Resume altogether, you can simply remove the key from
|
||||
your settings.
|
||||
<Trans>
|
||||
You have the option to{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
href="https://www.howtogeek.com/885918/how-to-get-an-openai-api-key/"
|
||||
>
|
||||
obtain your own OpenAI API key
|
||||
</a>
|
||||
. This key empowers you to leverage the API as you see fit. Alternatively, if you wish
|
||||
to disable the AI features in Reactive Resume altogether, you can simply remove the key
|
||||
from your settings.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -77,7 +80,7 @@ export const OpenAISettings = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Key</FormLabel>
|
||||
<FormLabel>{t`API Key`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="sk-..." {...field} />
|
||||
</FormControl>
|
||||
@ -95,13 +98,13 @@ export const OpenAISettings = () => {
|
||||
<Button type="submit" disabled={isEnabled || !form.formState.isDirty}>
|
||||
{!isEnabled && <LockSimpleOpen className="mr-2" />}
|
||||
{isEnabled && <LockSimple className="mr-2" />}
|
||||
{isEnabled ? "Saved" : "Save Locally"}
|
||||
{isEnabled ? t`Stored` : t`Store Locally`}
|
||||
</Button>
|
||||
|
||||
{isEnabled && (
|
||||
<Button type="reset" variant="ghost" onClick={onRemove}>
|
||||
<TrashSimple className="mr-2" />
|
||||
Remove
|
||||
{t`Forget`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@ -110,36 +113,38 @@ export const OpenAISettings = () => {
|
||||
|
||||
<div className="prose prose-sm prose-zinc max-w-full dark:prose-invert">
|
||||
<p>
|
||||
Your API key is securely stored in the browser's local storage and is only utilized when
|
||||
making requests to OpenAI via their official SDK. Rest assured that your key is not
|
||||
transmitted to any external server except when interacting with OpenAI's services.
|
||||
<Trans>
|
||||
Your API key is securely stored in the browser's local storage and is only utilized when
|
||||
making requests to OpenAI via their official SDK. Rest assured that your key is not
|
||||
transmitted to any external server except when interacting with OpenAI's services.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Alert variant="warning">
|
||||
<div className="prose prose-neutral max-w-full text-xs leading-relaxed text-primary dark:prose-invert">
|
||||
<span className="font-medium">Note: </span>
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="font-medium">Note: </span>
|
||||
By utilizing the OpenAI API, you acknowledge and accept the{" "}
|
||||
<a
|
||||
href="https://openai.com/policies/terms-of-use"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
terms of use
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href="https://openai.com/policies/privacy-policy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
privacy policy
|
||||
</a>{" "}
|
||||
outlined by OpenAI. Please note that Reactive Resume bears no responsibility for any
|
||||
improper or unauthorized utilization of the service, and any resulting repercussions or
|
||||
liabilities solely rest on the user.
|
||||
</span>
|
||||
</Trans>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { useTheme } from "@reactive-resume/hooks";
|
||||
import { Button } from "@reactive-resume/ui";
|
||||
import { Combobox } from "@reactive-resume/ui";
|
||||
@ -8,11 +9,12 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { dynamicActivate, getLocales } from "@/client/libs/lingui";
|
||||
import { useUpdateUser, useUser } from "@/client/services/user";
|
||||
|
||||
const formSchema = z.object({
|
||||
theme: z.enum(["system", "light", "dark"]).default("system"),
|
||||
language: z.string().default("en"),
|
||||
locale: z.string().default("en-US"),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
@ -24,7 +26,7 @@ export const ProfileSettings = () => {
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { theme, language: "en" },
|
||||
defaultValues: { theme, locale: "en-US" },
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -34,7 +36,7 @@ export const ProfileSettings = () => {
|
||||
const onReset = () => {
|
||||
if (!user) return;
|
||||
|
||||
form.reset({ theme, language: user.language ?? "en" });
|
||||
form.reset({ theme, locale: user.locale ?? "en-US" });
|
||||
};
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
@ -42,8 +44,9 @@ export const ProfileSettings = () => {
|
||||
|
||||
setTheme(data.theme);
|
||||
|
||||
if (user.language !== data.language) {
|
||||
await updateUser({ language: data.language });
|
||||
if (user.locale !== data.locale) {
|
||||
await dynamicActivate(data.locale);
|
||||
await updateUser({ locale: data.locale });
|
||||
}
|
||||
|
||||
form.reset(data);
|
||||
@ -52,9 +55,9 @@ export const ProfileSettings = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">Profile</h3>
|
||||
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`Profile`}</h3>
|
||||
<p className="leading-relaxed opacity-75">
|
||||
Here, you can update your profile to customize and personalize your experience.
|
||||
{t`Here, you can update your profile to customize and personalize your experience.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -65,16 +68,16 @@ export const ProfileSettings = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Theme</FormLabel>
|
||||
<FormLabel>{t`Theme`}</FormLabel>
|
||||
<div className="w-full">
|
||||
<Combobox
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
options={[
|
||||
{ label: "System", value: "system" },
|
||||
{ label: "Light", value: "light" },
|
||||
{ label: "Dark", value: "dark" },
|
||||
{ label: t`System`, value: "system" },
|
||||
{ label: t`Light`, value: "light" },
|
||||
{ label: t`Dark`, value: "dark" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@ -83,35 +86,35 @@ export const ProfileSettings = () => {
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="language"
|
||||
name="locale"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Language</FormLabel>
|
||||
<FormLabel>{t`Language`}</FormLabel>
|
||||
<div className="w-full">
|
||||
<Combobox
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
options={[
|
||||
{
|
||||
value: "en",
|
||||
label: <p>English</p>,
|
||||
},
|
||||
]}
|
||||
options={Object.entries(getLocales()).map(([value, label]) => ({
|
||||
label,
|
||||
value,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<FormDescription>
|
||||
<span>
|
||||
Don't see your language?{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
href="https://translate.rxresu.me/"
|
||||
className="font-medium underline underline-offset-2"
|
||||
>
|
||||
Help translate the app.
|
||||
</a>
|
||||
<Trans>
|
||||
Don't see your locale?{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
href="https://translate.rxresu.me/"
|
||||
className="font-medium underline underline-offset-2"
|
||||
>
|
||||
Help translate the app.
|
||||
</a>
|
||||
</Trans>
|
||||
</span>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
@ -125,10 +128,10 @@ export const ProfileSettings = () => {
|
||||
)}
|
||||
>
|
||||
<Button type="submit" disabled={loading}>
|
||||
Save Changes
|
||||
{t`Save Changes`}
|
||||
</Button>
|
||||
<Button type="reset" variant="ghost" onClick={onReset}>
|
||||
Reset
|
||||
{t`Discard`}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@ -29,7 +30,8 @@ const formSchema = z
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
path: ["confirmPassword"],
|
||||
message: "The passwords you entered do not match.",
|
||||
// eslint-disable-next-line lingui/t-call-in-function
|
||||
message: t`The passwords you entered do not match.`,
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
@ -54,7 +56,7 @@ export const SecuritySettings = () => {
|
||||
|
||||
toast({
|
||||
variant: "success",
|
||||
title: "Your password has been updated successfully.",
|
||||
title: t`Your password has been updated successfully.`,
|
||||
});
|
||||
|
||||
onReset();
|
||||
@ -63,16 +65,15 @@ export const SecuritySettings = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">Security</h3>
|
||||
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`Security`}</h3>
|
||||
<p className="leading-relaxed opacity-75">
|
||||
In this section, you can change your password and enable/disable two-factor
|
||||
authentication.
|
||||
{t`In this section, you can change your password and enable/disable two-factor authentication.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Accordion type="multiple" defaultValue={["password", "two-factor"]}>
|
||||
<AccordionItem value="password">
|
||||
<AccordionTrigger>Password</AccordionTrigger>
|
||||
<AccordionTrigger>{t`Password`}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6 sm:grid-cols-2">
|
||||
@ -81,7 +82,7 @@ export const SecuritySettings = () => {
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>New Password</FormLabel>
|
||||
<FormLabel>{t`New Password`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
@ -94,7 +95,7 @@ export const SecuritySettings = () => {
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm New Password</FormLabel>
|
||||
<FormLabel>{t`Confirm New Password`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} />
|
||||
</FormControl>
|
||||
@ -117,10 +118,10 @@ export const SecuritySettings = () => {
|
||||
className="flex items-center space-x-2 self-center sm:col-start-2"
|
||||
>
|
||||
<Button type="submit" disabled={loading}>
|
||||
Change Password
|
||||
{t`Change Password`}
|
||||
</Button>
|
||||
<Button type="reset" variant="ghost" onClick={onReset}>
|
||||
Reset
|
||||
{t`Discard`}
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -131,27 +132,31 @@ export const SecuritySettings = () => {
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="two-factor">
|
||||
<AccordionTrigger>Two-Factor Authentication</AccordionTrigger>
|
||||
<AccordionTrigger>{t`Two-Factor Authentication`}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{user?.twoFactorEnabled ? (
|
||||
<p className="mb-4 leading-relaxed opacity-75">
|
||||
<strong>Two-factor authentication is enabled.</strong> You will be asked to enter a
|
||||
code every time you sign in.
|
||||
<Trans>
|
||||
<strong>Two-factor authentication is enabled.</strong> You will be asked to enter
|
||||
a code every time you sign in.
|
||||
</Trans>
|
||||
</p>
|
||||
) : (
|
||||
<p className="mb-4 leading-relaxed opacity-75">
|
||||
<strong>Two-factor authentication is currently disabled.</strong> You can enable it
|
||||
by adding an authenticator app to your account.
|
||||
<Trans>
|
||||
<strong>Two-factor authentication is currently disabled.</strong> You can enable
|
||||
it by adding an authenticator app to your account.
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{user?.twoFactorEnabled ? (
|
||||
<Button variant="outline" onClick={() => open("delete")}>
|
||||
Disable 2FA
|
||||
{t`Disable 2FA`}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" onClick={() => open("create")}>
|
||||
Enable 2FA
|
||||
{t`Enable 2FA`}
|
||||
</Button>
|
||||
)}
|
||||
</AccordionContent>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { Separator } from "@reactive-resume/ui";
|
||||
import { motion } from "framer-motion";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
@ -11,7 +12,9 @@ import { SecuritySettings } from "./_sections/security";
|
||||
export const SettingsPage = () => (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Settings - Reactive Resume</title>
|
||||
<title>
|
||||
{t`Settings`} - {t`Reactive Resume`}
|
||||
</title>
|
||||
</Helmet>
|
||||
|
||||
<div className="max-w-2xl space-y-8 pb-12">
|
||||
@ -20,7 +23,7 @@ export const SettingsPage = () => (
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="text-4xl font-bold tracking-tight"
|
||||
>
|
||||
Settings
|
||||
{t`Settings`}
|
||||
</motion.h1>
|
||||
|
||||
<AccountSettings />
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { Separator } from "@reactive-resume/ui";
|
||||
|
||||
import { Copyright } from "@/client/components/copyright";
|
||||
@ -12,11 +13,10 @@ export const Footer = () => (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<Logo size={96} className="-ml-2" />
|
||||
|
||||
<h2 className="text-xl font-medium">Reactive Resume</h2>
|
||||
<h2 className="text-xl font-medium">{t`Reactive Resume`}</h2>
|
||||
|
||||
<p className="prose prose-sm prose-zinc leading-relaxed opacity-60 dark:prose-invert">
|
||||
A free and open-source resume builder that simplifies the tasks of creating, updating, and
|
||||
sharing your resume.
|
||||
{t`A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.`}
|
||||
</p>
|
||||
|
||||
<Copyright className="mt-6" />
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
|
||||
import { HeroSection } from "./sections/hero";
|
||||
@ -7,7 +8,9 @@ import { StatisticsSection } from "./sections/statistics";
|
||||
export const HomePage = () => (
|
||||
<main className="relative isolate mb-[400px] overflow-hidden bg-background">
|
||||
<Helmet>
|
||||
<title>Reactive Resume - A free and open-source resume builder</title>
|
||||
<title>
|
||||
{t`Reactive Resume`} - {t`A free and open-source resume builder`}
|
||||
</title>
|
||||
</Helmet>
|
||||
|
||||
<HeroSection />
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { Book, SignOut } from "@phosphor-icons/react";
|
||||
import { Button } from "@reactive-resume/ui";
|
||||
import { Link } from "react-router-dom";
|
||||
@ -14,12 +15,12 @@ export const HeroCTA = () => {
|
||||
return (
|
||||
<>
|
||||
<Button asChild size="lg">
|
||||
<Link to="/dashboard">Go to Dashboard</Link>
|
||||
<Link to="/dashboard">{t`Go to Dashboard`}</Link>
|
||||
</Button>
|
||||
|
||||
<Button size="lg" variant="link" onClick={() => logout()}>
|
||||
<SignOut className="mr-3" />
|
||||
Logout
|
||||
{t`Logout`}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
@ -29,13 +30,13 @@ export const HeroCTA = () => {
|
||||
return (
|
||||
<>
|
||||
<Button asChild size="lg">
|
||||
<Link to="/auth/login">Get started</Link>
|
||||
<Link to="/auth/login">{t`Get Started`}</Link>
|
||||
</Button>
|
||||
|
||||
<Button asChild size="lg" variant="link">
|
||||
<a href="https://docs.rxresu.me" target="_blank" rel="noopener noreferrer nofollow">
|
||||
<Book className="mr-3" />
|
||||
Learn more
|
||||
{t`Learn more`}
|
||||
</a>
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { ArrowRight } from "@phosphor-icons/react";
|
||||
import { Badge, Button } from "@reactive-resume/ui";
|
||||
import { motion } from "framer-motion";
|
||||
@ -20,24 +21,23 @@ export const HeroSection = () => (
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
>
|
||||
<div className="mt-24 flex items-center gap-x-4 sm:mt-32 lg:mt-0">
|
||||
<Badge>Version 4</Badge>
|
||||
<Badge>{t`Version 4`}</Badge>
|
||||
|
||||
<Button variant="link" className="space-x-2 text-left">
|
||||
<p>What's new in the latest version</p>
|
||||
<p>{t`What's new in the latest version`}</p>
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 space-y-2">
|
||||
<h6 className="text-base font-bold tracking-wide">Finally,</h6>
|
||||
<h6 className="text-base font-bold tracking-wide">{t`Finally,`}</h6>
|
||||
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
|
||||
A free and open-source resume builder.
|
||||
{t`A free and open-source resume builder.`}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p className="prose prose-base prose-zinc mt-6 text-lg leading-8 dark:prose-invert">
|
||||
A free and open-source resume builder that simplifies the process of creating, updating,
|
||||
and sharing your resume.
|
||||
{t`A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.`}
|
||||
</p>
|
||||
|
||||
<div className="mt-10 flex items-center gap-x-8">
|
||||
@ -53,7 +53,7 @@ export const HeroSection = () => (
|
||||
width={3600}
|
||||
height={2078}
|
||||
src="/screenshots/builder.png"
|
||||
alt="Reactive Resume - Screenshot - Builder Screen"
|
||||
alt={t`Reactive Resume - Screenshot - Builder Screen`}
|
||||
className="w-[76rem] rounded-lg bg-background/5 shadow-2xl ring-1 ring-foreground/10"
|
||||
/>
|
||||
</Tilt>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Button } from "@reactive-resume/ui";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { buttonVariants } from "@reactive-resume/ui";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
|
||||
type LogoProps = { company: string };
|
||||
@ -35,7 +36,7 @@ export const LogoCloudSection = () => (
|
||||
<section className="relative py-24 sm:py-32">
|
||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<p className="text-center text-lg leading-relaxed">
|
||||
Reactive Resume has helped people land jobs at these great companies:
|
||||
{t`Reactive Resume has helped people land jobs at these great companies:`}
|
||||
</p>
|
||||
<div className="mx-auto mt-10 grid max-w-lg grid-cols-4 items-center gap-x-8 gap-y-10 sm:max-w-xl sm:grid-cols-6 sm:gap-x-10 lg:mx-0 lg:max-w-none lg:grid-cols-5">
|
||||
{logoList.map((company) => (
|
||||
@ -43,17 +44,18 @@ export const LogoCloudSection = () => (
|
||||
))}
|
||||
</div>
|
||||
<p className="mx-auto mt-8 max-w-sm text-center leading-relaxed">
|
||||
If this app has helped you with your job hunt, let me know by reaching out through{" "}
|
||||
<Button asChild variant="link" className="p-0">
|
||||
<Trans>
|
||||
If this app has helped you with your job hunt, let me know by reaching out through{" "}
|
||||
<a
|
||||
href="https://www.amruthpillai.com/#contact"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
href="https://www.amruthpillai.com/#contact"
|
||||
className={cn(buttonVariants({ variant: "link" }), "p-0")}
|
||||
>
|
||||
this contact form
|
||||
</a>
|
||||
</Button>
|
||||
.
|
||||
.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { Counter } from "./counter";
|
||||
|
||||
type Statistic = {
|
||||
@ -5,25 +7,27 @@ type Statistic = {
|
||||
value: number;
|
||||
};
|
||||
|
||||
const stats: Statistic[] = [
|
||||
{ name: "GitHub Stars", value: 11800 },
|
||||
{ name: "Users Signed Up", value: 300000 },
|
||||
{ name: "Resumes Generated", value: 400000 },
|
||||
];
|
||||
export const StatisticsSection = () => {
|
||||
const stats: Statistic[] = [
|
||||
{ name: t`GitHub Stars`, value: 11800 },
|
||||
{ name: t`Users Signed Up`, value: 300000 },
|
||||
{ name: t`Resumes Generated`, value: 400000 },
|
||||
];
|
||||
|
||||
export const StatisticsSection = () => (
|
||||
<section className="relative py-24 sm:py-32">
|
||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<dl className="grid grid-cols-1 gap-x-8 gap-y-16 text-center lg:grid-cols-3">
|
||||
{stats.map((stat, index) => (
|
||||
<div key={index} className="mx-auto flex max-w-xs flex-col gap-y-4">
|
||||
<dt className="text-base leading-7 opacity-60">{stat.name}</dt>
|
||||
<dd className="order-first text-3xl font-semibold tracking-tight sm:text-5xl">
|
||||
<Counter from={0} to={stat.value} />+
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section className="relative py-24 sm:py-32">
|
||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<dl className="grid grid-cols-1 gap-x-8 gap-y-16 text-center lg:grid-cols-3">
|
||||
{stats.map((stat, index) => (
|
||||
<div key={index} className="mx-auto flex max-w-xs flex-col gap-y-4">
|
||||
<dt className="text-base leading-7 opacity-60">{stat.name}</dt>
|
||||
<dd className="order-first text-3xl font-semibold tracking-tight sm:text-5xl">
|
||||
<Counter from={0} to={stat.value} />+
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { ResumeDto } from "@reactive-resume/dto";
|
||||
import { Button } from "@reactive-resume/ui";
|
||||
import { pageSizeMap } from "@reactive-resume/utils";
|
||||
@ -52,7 +53,9 @@ export const PublicResumePage = () => {
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>{title} - Reactive Resume</title>
|
||||
<title>
|
||||
{title} - {t`Reactive Resume`}
|
||||
</title>
|
||||
</Helmet>
|
||||
|
||||
<div
|
||||
@ -70,9 +73,9 @@ export const PublicResumePage = () => {
|
||||
<div className="flex justify-center py-10 opacity-50 print:hidden">
|
||||
<Link to="/">
|
||||
<Button size="sm" variant="ghost" className="space-x-1.5 text-xs font-normal">
|
||||
<span>Built with</span>
|
||||
<span>{t`Built with`}</span>
|
||||
<Icon size={12} />
|
||||
<span>Reactive Resume</span>
|
||||
<span>{t`Reactive Resume`}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@ -98,7 +101,7 @@ export const publicLoader: LoaderFunction<ResumeDto> = async ({ params }) => {
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "error",
|
||||
title: "The resume you were looking for was nowhere to be found... or maybe never existed?",
|
||||
title: t`The resume you were looking for doesn't seem to exist, please check the link and try again.`,
|
||||
});
|
||||
|
||||
return redirect("/");
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import "@/client/libs/dayjs";
|
||||
|
||||
import { TooltipProvider } from "@reactive-resume/ui";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { HelmetProvider } from "react-helmet-async";
|
||||
@ -7,21 +5,24 @@ import { Outlet } from "react-router-dom";
|
||||
|
||||
import { queryClient } from "../libs/query-client";
|
||||
import { DialogProvider } from "./dialog";
|
||||
import { LocaleProvider } from "./locale";
|
||||
import { ThemeProvider } from "./theme";
|
||||
import { Toaster } from "./toaster";
|
||||
|
||||
export const Providers = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<TooltipProvider>
|
||||
<DialogProvider>
|
||||
<HelmetProvider>
|
||||
<Outlet />
|
||||
<LocaleProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<TooltipProvider>
|
||||
<DialogProvider>
|
||||
<HelmetProvider>
|
||||
<Outlet />
|
||||
|
||||
<Toaster />
|
||||
</HelmetProvider>
|
||||
</DialogProvider>
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
<Toaster />
|
||||
</HelmetProvider>
|
||||
</DialogProvider>
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</LocaleProvider>
|
||||
);
|
||||
|
||||
27
apps/client/src/providers/locale.tsx
Normal file
27
apps/client/src/providers/locale.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import "@/client/libs/dayjs";
|
||||
|
||||
import { i18n } from "@lingui/core";
|
||||
import { detect, fromNavigator, fromStorage, fromUrl } from "@lingui/detect-locale";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { defaultLocale, dynamicActivate } from "../libs/lingui";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const LocaleProvider = ({ children }: Props) => {
|
||||
useEffect(() => {
|
||||
const detectedLocale = detect(
|
||||
fromUrl("lang"),
|
||||
fromStorage("lang"),
|
||||
fromNavigator(),
|
||||
defaultLocale,
|
||||
)!;
|
||||
|
||||
dynamicActivate(detectedLocale);
|
||||
}, []);
|
||||
|
||||
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
|
||||
};
|
||||
@ -1,3 +1,7 @@
|
||||
/* eslint-disable lingui/text-restrictions */
|
||||
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { openai } from "./client";
|
||||
|
||||
const PROMPT = `You are an AI writing assistant specialized in writing copy for resumes.
|
||||
@ -23,7 +27,7 @@ export const changeTone = async (text: string, mood: Mood) => {
|
||||
});
|
||||
|
||||
if (result.choices.length === 0) {
|
||||
throw new Error("OpenAI did not return any choices for your text.");
|
||||
throw new Error(t`OpenAI did not return any choices for your text.`);
|
||||
}
|
||||
|
||||
return result.choices[0].text;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { OpenAI } from "openai";
|
||||
|
||||
import { useOpenAiStore } from "@/client/stores/openai";
|
||||
@ -7,7 +8,7 @@ export const openai = () => {
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
"Your OpenAI API Key has not been set yet. Please go to your account settings to enable OpenAI Integration.",
|
||||
t`Your OpenAI API Key has not been set yet. Please go to your account settings to enable OpenAI Integration.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
/* eslint-disable lingui/text-restrictions */
|
||||
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { openai } from "./client";
|
||||
|
||||
const PROMPT = `You are an AI writing assistant specialized in writing copy for resumes.
|
||||
@ -21,7 +25,7 @@ export const fixGrammar = async (text: string) => {
|
||||
});
|
||||
|
||||
if (result.choices.length === 0) {
|
||||
throw new Error("OpenAI did not return any choices for your text.");
|
||||
throw new Error(t`OpenAI did not return any choices for your text.`);
|
||||
}
|
||||
|
||||
return result.choices[0].text;
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
/* eslint-disable lingui/text-restrictions */
|
||||
|
||||
import { t } from "@lingui/macro";
|
||||
|
||||
import { openai } from "./client";
|
||||
|
||||
const PROMPT = `You are an AI writing assistant specialized in writing copy for resumes.
|
||||
@ -21,7 +25,7 @@ export const improveWriting = async (text: string) => {
|
||||
});
|
||||
|
||||
if (result.choices.length === 0) {
|
||||
throw new Error("OpenAI did not return any choices for your text.");
|
||||
throw new Error(t`OpenAI did not return any choices for your text.`);
|
||||
}
|
||||
|
||||
return result.choices[0].text;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { StatisticsDto, UrlDto } from "@reactive-resume/dto";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
@ -25,11 +26,6 @@ export const usePrintResume = () => {
|
||||
if (cache === undefined) return cache;
|
||||
return { ...cache, downloads: cache.downloads + 1 } satisfies StatisticsDto;
|
||||
});
|
||||
|
||||
toast({
|
||||
variant: "success",
|
||||
title: "A PDF of your resume has been successfully generated.",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof AxiosError) {
|
||||
@ -37,7 +33,7 @@ export const usePrintResume = () => {
|
||||
|
||||
toast({
|
||||
variant: "error",
|
||||
title: "An error occurred while trying to generate your resume.",
|
||||
title: t`An error occurred while trying to print your resume.`,
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { ResumeDto, UpdateResumeDto } from "@reactive-resume/dto";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
@ -31,7 +32,7 @@ export const updateResume = async (data: UpdateResumeDto) => {
|
||||
|
||||
toast({
|
||||
variant: "error",
|
||||
title: "There was an error while updating your resume.",
|
||||
title: t`There was an error while updating your resume.`,
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { ResumeDto } from "@reactive-resume/dto";
|
||||
import { CustomSectionGroup, defaultSection, SectionKey } from "@reactive-resume/schema";
|
||||
@ -41,7 +42,7 @@ export const useResumeStore = create<ResumeStore>()(
|
||||
const section: CustomSectionGroup = {
|
||||
...defaultSection,
|
||||
id: createId(),
|
||||
name: "Custom Section",
|
||||
name: t`Custom Section`,
|
||||
items: [],
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/// <reference types='vitest' />
|
||||
|
||||
import { lingui } from "@lingui/vite-plugin";
|
||||
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import { defineConfig, searchForWorkspaceRoot, splitVendorChunkPlugin } from "vite";
|
||||
@ -21,7 +22,14 @@ export default defineConfig({
|
||||
fs: { allow: [searchForWorkspaceRoot(process.cwd())] },
|
||||
},
|
||||
|
||||
plugins: [react(), nxViteTsPaths(), splitVendorChunkPlugin()],
|
||||
plugins: [
|
||||
react({
|
||||
plugins: [["@lingui/swc-plugin", {}]],
|
||||
}),
|
||||
lingui(),
|
||||
nxViteTsPaths(),
|
||||
splitVendorChunkPlugin(),
|
||||
],
|
||||
|
||||
test: {
|
||||
globals: true,
|
||||
|
||||
@ -100,7 +100,7 @@ export class AuthService {
|
||||
name: registerDto.name,
|
||||
email: registerDto.email,
|
||||
username: registerDto.username,
|
||||
language: registerDto.language,
|
||||
locale: registerDto.locale,
|
||||
provider: "email",
|
||||
emailVerified: false, // Set to true if you don't want to verify user's email
|
||||
secrets: { create: { password: hashedPassword } },
|
||||
|
||||
@ -44,7 +44,7 @@ export class GitHubStrategy extends PassportStrategy(Strategy, "github") {
|
||||
user = await this.userService.create({
|
||||
email,
|
||||
picture,
|
||||
language: "en",
|
||||
locale: "en-US",
|
||||
name: displayName,
|
||||
provider: "github",
|
||||
emailVerified: true, // auto-verify emails
|
||||
|
||||
@ -44,7 +44,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, "google") {
|
||||
user = await this.userService.create({
|
||||
email,
|
||||
picture,
|
||||
language: "en",
|
||||
locale: "en-US",
|
||||
name: displayName,
|
||||
provider: "google",
|
||||
emailVerified: true, // auto-verify emails
|
||||
|
||||
@ -41,7 +41,7 @@ export class UserController {
|
||||
name: updateUserDto.name,
|
||||
picture: updateUserDto.picture,
|
||||
username: updateUserDto.username,
|
||||
language: updateUserDto.language,
|
||||
locale: updateUserDto.locale,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
11
crowdin.yml
Normal file
11
crowdin.yml
Normal file
@ -0,0 +1,11 @@
|
||||
base_path: .
|
||||
project_id: "503410"
|
||||
base_url: https://api.crowdin.com
|
||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||
|
||||
preserve_hierarchy: false
|
||||
|
||||
files:
|
||||
- type: po
|
||||
source: /apps/client/src/locales/en-US.po
|
||||
translation: /apps/client/src/locales/%locale%.po
|
||||
@ -4,7 +4,7 @@ import { z } from "nestjs-zod/z";
|
||||
import { userSchema } from "../user";
|
||||
|
||||
export const registerSchema = userSchema
|
||||
.pick({ name: true, email: true, username: true, language: true })
|
||||
.pick({ name: true, email: true, username: true, locale: true })
|
||||
.extend({ password: z.password().min(6) });
|
||||
|
||||
export class RegisterDto extends createZodDto(registerSchema) {}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user