mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-19 03:01:53 +10:00
feat(i18n): implement localization using LinguiJS
This commit is contained in:
@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user