mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-19 03:01:53 +10:00
release: v4.1.0
This commit is contained in:
@ -27,12 +27,12 @@ const ActiveIndicator = ({ className }: Props) => (
|
||||
/>
|
||||
);
|
||||
|
||||
interface SidebarItem {
|
||||
type SidebarItem = {
|
||||
path: string;
|
||||
name: string;
|
||||
shortcut?: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
};
|
||||
|
||||
type SidebarItemProps = SidebarItem & {
|
||||
onClick?: () => void;
|
||||
@ -46,11 +46,11 @@ const SidebarItem = ({ path, name, shortcut, icon, onClick }: SidebarItemProps)
|
||||
asChild
|
||||
size="lg"
|
||||
variant="ghost"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"h-auto justify-start px-4 py-3",
|
||||
isActive && "pointer-events-none bg-secondary/50 text-secondary-foreground",
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Link to={path}>
|
||||
<div className="mr-3">{icon}</div>
|
||||
|
||||
@ -77,6 +77,9 @@ export const ImportDialog = () => {
|
||||
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
type: ImportType["reactive-resume-json"],
|
||||
},
|
||||
resolver: zodResolver(formSchema),
|
||||
});
|
||||
const filetype = form.watch("type");
|
||||
@ -91,7 +94,6 @@ export const ImportDialog = () => {
|
||||
}, [filetype]);
|
||||
|
||||
const accept = useMemo(() => {
|
||||
if (!filetype) return "";
|
||||
if (filetype.includes("json")) return ".json";
|
||||
if (filetype.includes("zip")) return ".zip";
|
||||
return "";
|
||||
@ -255,11 +257,11 @@ export const ImportDialog = () => {
|
||||
<FormLabel>{t`File`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="file"
|
||||
key={`${accept}-${filetype}`}
|
||||
type="file"
|
||||
accept={accept}
|
||||
onChange={(event) => {
|
||||
if (!event.target.files || !event.target.files.length) return;
|
||||
if (!event.target.files?.length) return;
|
||||
field.onChange(event.target.files[0]);
|
||||
}}
|
||||
/>
|
||||
@ -278,7 +280,7 @@ export const ImportDialog = () => {
|
||||
)}
|
||||
/>
|
||||
|
||||
{validationResult?.isValid === false && validationResult.errors !== undefined && (
|
||||
{validationResult?.isValid === false && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-error">{t`Errors`}</Label>
|
||||
<ScrollArea orientation="vertical" className="h-[180px]">
|
||||
@ -291,7 +293,7 @@ export const ImportDialog = () => {
|
||||
|
||||
<DialogFooter>
|
||||
<AnimatePresence presenceAffectsLayout>
|
||||
{(!validationResult || false) && (
|
||||
{!validationResult && (
|
||||
<Button type="button" onClick={onValidate}>
|
||||
{t`Validate`}
|
||||
</Button>
|
||||
@ -305,7 +307,7 @@ export const ImportDialog = () => {
|
||||
|
||||
{validationResult !== null && validationResult.isValid && (
|
||||
<>
|
||||
<Button type="button" onClick={onImport} disabled={loading}>
|
||||
<Button type="button" disabled={loading} onClick={onImport}>
|
||||
{t`Import`}
|
||||
</Button>
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@ export const ResumeDialog = () => {
|
||||
if (isDelete) {
|
||||
if (!payload.item?.id) return;
|
||||
|
||||
await deleteResume({ id: payload.item?.id });
|
||||
await deleteResume({ id: payload.item.id });
|
||||
}
|
||||
|
||||
close();
|
||||
@ -131,7 +131,7 @@ export const ResumeDialog = () => {
|
||||
|
||||
await duplicateResume({
|
||||
title: title || randomName,
|
||||
slug: slug || kebabCase(randomName),
|
||||
slug: slug ?? kebabCase(randomName),
|
||||
data: sampleResume,
|
||||
});
|
||||
|
||||
|
||||
@ -13,11 +13,11 @@ type Props = {
|
||||
export const BaseCard = ({ children, className, onClick }: Props) => (
|
||||
<Tilt {...defaultTiltProps}>
|
||||
<Card
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"relative flex aspect-[1/1.4142] scale-100 cursor-pointer items-center justify-center bg-secondary/50 p-0 transition-transform active:scale-95",
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
|
||||
@ -11,7 +11,11 @@ export const CreateResumeCard = () => {
|
||||
const { open } = useDialog("resume");
|
||||
|
||||
return (
|
||||
<BaseCard onClick={() => open("create")}>
|
||||
<BaseCard
|
||||
onClick={() => {
|
||||
open("create");
|
||||
}}
|
||||
>
|
||||
<Plus size={64} weight="thin" />
|
||||
|
||||
<div
|
||||
|
||||
@ -11,7 +11,11 @@ export const ImportResumeCard = () => {
|
||||
const { open } = useDialog("import");
|
||||
|
||||
return (
|
||||
<BaseCard onClick={() => open("create")}>
|
||||
<BaseCard
|
||||
onClick={() => {
|
||||
open("create");
|
||||
}}
|
||||
>
|
||||
<DownloadSimple size={64} weight="thin" />
|
||||
|
||||
<div
|
||||
|
||||
@ -62,7 +62,7 @@ export const ResumeCard = ({ resume }: Props) => {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<BaseCard onClick={onOpen} className="space-y-0">
|
||||
<BaseCard className="space-y-0" onClick={onOpen}>
|
||||
<AnimatePresence presenceAffectsLayout>
|
||||
{loading && (
|
||||
<motion.div
|
||||
@ -88,7 +88,7 @@ export const ResumeCard = ({ resume }: Props) => {
|
||||
loading="lazy"
|
||||
alt={resume.title}
|
||||
className="size-full object-cover"
|
||||
src={`${url}?cache=${new Date().getTime()}`}
|
||||
src={`${url}?cache=${Date.now()}`}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@ -143,7 +143,7 @@ export const ResumeCard = ({ resume }: Props) => {
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onClick={onDelete} className="text-error">
|
||||
<ContextMenuItem className="text-error" onClick={onDelete}>
|
||||
<TrashSimple size={14} className="mr-2" />
|
||||
{t`Delete`}
|
||||
</ContextMenuItem>
|
||||
|
||||
@ -25,7 +25,7 @@ export const GridView = () => {
|
||||
</motion.div>
|
||||
|
||||
{loading &&
|
||||
[...Array(4)].map((_, i) => (
|
||||
Array.from({ length: 4 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="duration-300 animate-in fade-in"
|
||||
@ -41,8 +41,8 @@ export const GridView = () => {
|
||||
.sort((a, b) => sortByDate(a, b, "updatedAt"))
|
||||
.map((resume, index) => (
|
||||
<motion.div
|
||||
layout
|
||||
key={resume.id}
|
||||
layout
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
animate={{ opacity: 1, x: 0, transition: { delay: (index + 2) * 0.1 } }}
|
||||
exit={{ opacity: 0, filter: "blur(8px)", transition: { duration: 0.5 } }}
|
||||
|
||||
@ -11,11 +11,11 @@ type Props = {
|
||||
|
||||
export const BaseListItem = ({ title, description, start, end, className, onClick }: Props) => (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center rounded p-4 transition-colors hover:bg-secondary/30",
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
|
||||
@ -13,7 +13,6 @@ export const CreateResumeListItem = () => {
|
||||
return (
|
||||
<BaseListItem
|
||||
start={<Plus size={18} />}
|
||||
onClick={() => open("create")}
|
||||
title={
|
||||
<>
|
||||
<span>{t`Create a new resume`}</span>
|
||||
@ -22,6 +21,9 @@ export const CreateResumeListItem = () => {
|
||||
</>
|
||||
}
|
||||
description={t`Start building from scratch`}
|
||||
onClick={() => {
|
||||
open("create");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -12,7 +12,6 @@ export const ImportResumeListItem = () => {
|
||||
return (
|
||||
<BaseListItem
|
||||
start={<DownloadSimple size={18} />}
|
||||
onClick={() => open("create")}
|
||||
title={
|
||||
<>
|
||||
<span>{t`Import an existing resume`}</span>
|
||||
@ -21,6 +20,9 @@ export const ImportResumeListItem = () => {
|
||||
</>
|
||||
}
|
||||
description={t`LinkedIn, JSON Resume, etc.`}
|
||||
onClick={() => {
|
||||
open("create");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -143,11 +143,11 @@ export const ResumeListItem = ({ resume }: Props) => {
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
<BaseListItem
|
||||
onClick={onOpen}
|
||||
className="group"
|
||||
title={resume.title}
|
||||
description={t`Last updated ${lastUpdated}`}
|
||||
end={dropdownMenu}
|
||||
onClick={onOpen}
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent align="end" className="p-0" sideOffset={-100} alignOffset={100}>
|
||||
@ -160,7 +160,7 @@ export const ResumeListItem = ({ resume }: Props) => {
|
||||
loading="lazy"
|
||||
alt={resume.title}
|
||||
className="aspect-[1/1.4142] w-60 rounded-sm object-cover"
|
||||
src={`${url}?cache=${new Date().getTime()}`}
|
||||
src={`${url}?cache=${Date.now()}`}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@ -193,7 +193,7 @@ export const ResumeListItem = ({ resume }: Props) => {
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onClick={onDelete} className="text-error">
|
||||
<ContextMenuItem className="text-error" onClick={onDelete}>
|
||||
<TrashSimple size={14} className="mr-2" />
|
||||
{t`Delete`}
|
||||
</ContextMenuItem>
|
||||
|
||||
@ -25,7 +25,7 @@ export const ListView = () => {
|
||||
</motion.div>
|
||||
|
||||
{loading &&
|
||||
[...Array(4)].map((_, i) => (
|
||||
Array.from({ length: 4 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="duration-300 animate-in fade-in"
|
||||
|
||||
@ -23,8 +23,10 @@ export const ResumesPage = () => {
|
||||
|
||||
<Tabs
|
||||
value={layout}
|
||||
onValueChange={(value) => setLayout(value as Layout)}
|
||||
className="space-y-4"
|
||||
onValueChange={(value) => {
|
||||
setLayout(value as Layout);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<motion.h1
|
||||
|
||||
@ -38,7 +38,7 @@ import { queryClient } from "@/client/libs/query-client";
|
||||
import { useDisable2FA, useEnable2FA, useSetup2FA } from "@/client/services/auth";
|
||||
import { useDialog } from "@/client/stores/dialog";
|
||||
|
||||
// We're using the pre-existing "mode" state to determine the stage of 2FA setup the user is in.
|
||||
// We're using the pre-existing "mode" state to determine the stage of 2FA set up the user is in.
|
||||
// - "create" mode is used to enable 2FA.
|
||||
// - "update" mode is used to verify 2FA, displaying a QR Code, once enabled.
|
||||
// - "duplicate" mode is used to display the backup codes after initial verification.
|
||||
@ -81,7 +81,7 @@ export const TwoFactorDialog = () => {
|
||||
form.setValue("uri", data.message);
|
||||
};
|
||||
|
||||
if (isCreate) initialize();
|
||||
if (isCreate) void initialize();
|
||||
}, [isCreate]);
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
@ -232,7 +232,12 @@ export const TwoFactorDialog = () => {
|
||||
{isCreate && <Button disabled={loading}>{t`Continue`}</Button>}
|
||||
{isUpdate && (
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => open("create")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
open("create");
|
||||
}}
|
||||
>
|
||||
{t`Back`}
|
||||
</Button>
|
||||
|
||||
|
||||
@ -108,7 +108,7 @@ export const AccountSettings = () => {
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6 sm:grid-cols-2">
|
||||
<form className="grid gap-6 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
name="picture"
|
||||
control={form.control}
|
||||
@ -119,22 +119,22 @@ export const AccountSettings = () => {
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>{t`Picture`}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://..." {...field} value={field.value || ""} />
|
||||
<Input placeholder="https://..." {...field} value={field.value ?? ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
{!user.picture && (
|
||||
<>
|
||||
<input hidden type="file" ref={inputRef} onChange={onSelectImage} />
|
||||
<input ref={inputRef} hidden type="file" onChange={onSelectImage} />
|
||||
|
||||
<motion.button
|
||||
disabled={isUploading}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
className={cn(buttonVariants({ size: "icon", variant: "ghost" }))}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
<UploadSimple />
|
||||
</motion.button>
|
||||
|
||||
@ -70,7 +70,7 @@ export const DangerZoneSettings = () => {
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onDelete)} className="grid gap-6 sm:grid-cols-2">
|
||||
<form className="grid gap-6 sm:grid-cols-2" onSubmit={form.handleSubmit(onDelete)}>
|
||||
<FormField
|
||||
name="deleteConfirm"
|
||||
control={form.control}
|
||||
|
||||
@ -22,7 +22,7 @@ const formSchema = z.object({
|
||||
apiKey: z
|
||||
.string()
|
||||
// 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.`)
|
||||
.regex(/^sk-[\dA-Za-z]+$/, t`That doesn't look like a valid OpenAI API key.`)
|
||||
.default(""),
|
||||
});
|
||||
|
||||
@ -37,7 +37,7 @@ export const OpenAISettings = () => {
|
||||
defaultValues: { apiKey: apiKey ?? "" },
|
||||
});
|
||||
|
||||
const onSubmit = async ({ apiKey }: FormValues) => {
|
||||
const onSubmit = ({ apiKey }: FormValues) => {
|
||||
setApiKey(apiKey);
|
||||
};
|
||||
|
||||
@ -74,7 +74,7 @@ export const OpenAISettings = () => {
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6 sm:grid-cols-2">
|
||||
<form className="grid gap-6 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
name="apiKey"
|
||||
control={form.control}
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
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";
|
||||
import { Form, FormDescription, FormField, FormItem, FormLabel } from "@reactive-resume/ui";
|
||||
import {
|
||||
Button,
|
||||
Combobox,
|
||||
Form,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@reactive-resume/ui";
|
||||
import { cn } from "@reactive-resume/utils";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@ -36,7 +42,7 @@ export const ProfileSettings = () => {
|
||||
const onReset = () => {
|
||||
if (!user) return;
|
||||
|
||||
form.reset({ theme, locale: user.locale ?? "en-US" });
|
||||
form.reset({ theme, locale: user.locale });
|
||||
};
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
@ -64,7 +70,7 @@ export const ProfileSettings = () => {
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6 sm:grid-cols-2">
|
||||
<form className="grid gap-6 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
name="theme"
|
||||
control={form.control}
|
||||
@ -75,12 +81,12 @@ export const ProfileSettings = () => {
|
||||
<Combobox
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
options={[
|
||||
{ label: t`System`, value: "system" },
|
||||
{ label: t`Light`, value: "light" },
|
||||
{ label: t`Dark`, value: "dark" },
|
||||
]}
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
@ -76,7 +76,7 @@ export const SecuritySettings = () => {
|
||||
<AccordionTrigger>{t`Password`}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6 sm:grid-cols-2">
|
||||
<form className="grid gap-6 sm:grid-cols-2" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
name="password"
|
||||
control={form.control}
|
||||
@ -101,7 +101,7 @@ export const SecuritySettings = () => {
|
||||
</FormControl>
|
||||
{fieldState.error && (
|
||||
<FormDescription className="text-error-foreground">
|
||||
{fieldState.error?.message}
|
||||
{fieldState.error.message}
|
||||
</FormDescription>
|
||||
)}
|
||||
</FormItem>
|
||||
@ -151,11 +151,21 @@ export const SecuritySettings = () => {
|
||||
)}
|
||||
|
||||
{user?.twoFactorEnabled ? (
|
||||
<Button variant="outline" onClick={() => open("delete")}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
open("delete");
|
||||
}}
|
||||
>
|
||||
{t`Disable 2FA`}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" onClick={() => open("create")}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
open("create");
|
||||
}}
|
||||
>
|
||||
{t`Enable 2FA`}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user