release: v4.1.0

This commit is contained in:
Amruth Pillai
2024-05-05 14:55:06 +02:00
parent 68252c35fc
commit e87b05a93a
282 changed files with 11461 additions and 10713 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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,
});

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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 } }}

View File

@ -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">

View File

@ -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");
}}
/>
);
};

View File

@ -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");
}}
/>
);
};

View File

@ -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>

View File

@ -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"

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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>
)}