- Reactive Resume is free and open source. If it has helped you, please consider donating.
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/dialogs/api-key/create.tsx b/apps/web/src/dialogs/api-key/create.tsx
index d8a9471f8..68b6295f1 100644
--- a/apps/web/src/dialogs/api-key/create.tsx
+++ b/apps/web/src/dialogs/api-key/create.tsx
@@ -5,7 +5,6 @@ import { CopyIcon, PlusIcon } from "@phosphor-icons/react";
import { useStore } from "@tanstack/react-form";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
-import { toast } from "sonner";
import { useCopyToClipboard } from "usehooks-ts";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
@@ -24,6 +23,7 @@ import {
InputGroupButton,
InputGroupInput,
} from "@reactive-resume/ui/components/input-group";
+import { toast } from "@reactive-resume/ui/components/toast";
import { Combobox } from "@/components/ui/combobox";
import { useFormBlocker } from "@/hooks/use-form-blocker";
import { authClient } from "@/libs/auth/client";
@@ -56,7 +56,7 @@ const CreateApiKeyForm = ({ setApiKey }: CreateApiKeyFormProps) => {
},
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
- const toastId = toast.loading(t`Creating your API key...`);
+ const toastId = toast.add({ type: "loading", description: t`Creating your API key...` });
const { data, error } = await authClient.apiKey.create({
name: value.name,
@@ -64,21 +64,22 @@ const CreateApiKeyForm = ({ setApiKey }: CreateApiKeyFormProps) => {
});
if (error) {
- toast.error(
- getReadableErrorMessage(
+ toast.add({
+ type: "error",
+ description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when creating an API key fails",
message: "Failed to create API key. Please try again.",
}),
),
- { id: toastId },
- );
+ id: toastId,
+ });
return;
}
setApiKey(data.key);
- toast.dismiss(toastId);
+ toast.close(toastId);
},
});
@@ -199,7 +200,7 @@ const CopyApiKeyForm = ({ apiKey }: CopyApiKeyFormProps) => {
const onCopy = async () => {
await copyToClipboard(apiKey);
- toast.success(t`Your API key has been copied to the clipboard.`);
+ toast.add({ type: "success", description: t`Your API key has been copied to the clipboard.` });
};
const onConfirm = () => {
diff --git a/apps/web/src/dialogs/auth/change-password.tsx b/apps/web/src/dialogs/auth/change-password.tsx
index c95bf982b..19b98a6c0 100644
--- a/apps/web/src/dialogs/auth/change-password.tsx
+++ b/apps/web/src/dialogs/auth/change-password.tsx
@@ -3,7 +3,6 @@ import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { EyeIcon, EyeSlashIcon, PasswordIcon } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query";
-import { toast } from "sonner";
import { useToggle } from "usehooks-ts";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
@@ -16,6 +15,7 @@ import {
} from "@reactive-resume/ui/components/dialog";
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
+import { toast } from "@reactive-resume/ui/components/toast";
import { useFormBlocker } from "@/hooks/use-form-blocker";
import { authClient } from "@/libs/auth/client";
import { getReadableErrorMessage } from "@/libs/error-message";
@@ -48,7 +48,7 @@ export function ChangePasswordDialog(_: DialogProps<"auth.change-password">) {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
- const toastId = toast.loading(t`Updating your password...`);
+ const toastId = toast.add({ type: "loading", description: t`Updating your password...` });
const { error } = await authClient.changePassword({
currentPassword: value.currentPassword,
@@ -56,20 +56,21 @@ export function ChangePasswordDialog(_: DialogProps<"auth.change-password">) {
});
if (error) {
- toast.error(
- getReadableErrorMessage(
+ toast.add({
+ type: "error",
+ description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when changing account password fails",
message: "Failed to update your password. Please try again.",
}),
),
- { id: toastId },
- );
+ id: toastId,
+ });
return;
}
- toast.success(t`Your password has been updated successfully.`, { id: toastId });
+ toast.add({ type: "success", description: t`Your password has been updated successfully.`, id: toastId });
void queryClient.invalidateQueries({ queryKey: ["auth", "accounts"] });
closeDialog();
},
diff --git a/apps/web/src/dialogs/auth/disable-two-factor.tsx b/apps/web/src/dialogs/auth/disable-two-factor.tsx
index 35059410f..0b2b68c50 100644
--- a/apps/web/src/dialogs/auth/disable-two-factor.tsx
+++ b/apps/web/src/dialogs/auth/disable-two-factor.tsx
@@ -3,7 +3,6 @@ import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { EyeIcon, EyeSlashIcon, LockOpenIcon } from "@phosphor-icons/react";
import { useRouter } from "@tanstack/react-router";
-import { toast } from "sonner";
import { useToggle } from "usehooks-ts";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
@@ -16,6 +15,7 @@ import {
} from "@reactive-resume/ui/components/dialog";
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
+import { toast } from "@reactive-resume/ui/components/toast";
import { useFormBlocker } from "@/hooks/use-form-blocker";
import { authClient } from "@/libs/auth/client";
import { getReadableErrorMessage } from "@/libs/error-message";
@@ -35,25 +35,33 @@ export function DisableTwoFactorDialog(_: DialogProps<"auth.two-factor.disable">
defaultValues: { password: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
- const toastId = toast.loading(t`Disabling two-factor authentication...`);
+ const toastId = toast.add({
+ type: "loading",
+ description: t`Disabling two-factor authentication...`,
+ });
const { error } = await authClient.twoFactor.disable({ password: value.password });
if (error) {
- toast.error(
- getReadableErrorMessage(
+ toast.add({
+ type: "error",
+ description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when disabling two-factor authentication fails",
message: "Failed to disable two-factor authentication. Please try again.",
}),
),
- { id: toastId },
- );
+ id: toastId,
+ });
return;
}
- toast.success(t`Two-factor authentication has been disabled successfully.`, { id: toastId });
+ toast.add({
+ type: "success",
+ description: t`Two-factor authentication has been disabled successfully.`,
+ id: toastId,
+ });
void router.invalidate();
closeDialog();
form.reset();
diff --git a/apps/web/src/dialogs/auth/enable-two-factor.tsx b/apps/web/src/dialogs/auth/enable-two-factor.tsx
index 23e13b247..0e2733189 100644
--- a/apps/web/src/dialogs/auth/enable-two-factor.tsx
+++ b/apps/web/src/dialogs/auth/enable-two-factor.tsx
@@ -6,7 +6,6 @@ import { useStore } from "@tanstack/react-form";
import { useRouter } from "@tanstack/react-router";
import { QRCodeSVG } from "qrcode.react";
import { useState } from "react";
-import { toast } from "sonner";
import { match } from "ts-pattern";
import { useToggle } from "usehooks-ts";
import z from "zod";
@@ -20,6 +19,7 @@ import {
} from "@reactive-resume/ui/components/dialog";
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
+import { toast } from "@reactive-resume/ui/components/toast";
import { useFormBlocker } from "@/hooks/use-form-blocker";
import { authClient } from "@/libs/auth/client";
import { getReadableErrorMessage } from "@/libs/error-message";
@@ -58,7 +58,7 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
defaultValues: { password: "" },
validators: { onSubmit: enableFormSchema },
onSubmit: async ({ value }) => {
- const toastId = toast.loading(t`Enabling two-factor authentication…`);
+ const toastId = toast.add({ type: "loading", description: t`Enabling two-factor authentication…` });
const { data, error } = await authClient.twoFactor.enable({
password: value.password,
@@ -66,16 +66,17 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
});
if (error) {
- toast.error(
- getReadableErrorMessage(
+ toast.add({
+ type: "error",
+ description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when enabling two-factor authentication fails",
message: "Failed to enable two-factor authentication. Please try again.",
}),
),
- { id: toastId },
- );
+ id: toastId,
+ });
return;
}
@@ -83,9 +84,9 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
setTotpUri(data.totpURI);
setBackupCodes(data.backupCodes);
setStep("verify");
- toast.dismiss(toastId);
+ toast.close(toastId);
} else {
- toast.error(t`Failed to setup two-factor authentication.`, { id: toastId });
+ toast.add({ type: "error", description: t`Failed to setup two-factor authentication.`, id: toastId });
}
},
});
@@ -94,25 +95,26 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
defaultValues: { code: "" },
validators: { onSubmit: verifyFormSchema },
onSubmit: async ({ value }) => {
- const toastId = toast.loading(t`Verifying code…`);
+ const toastId = toast.add({ type: "loading", description: t`Verifying code…` });
const { error } = await authClient.twoFactor.verifyTotp({ code: value.code });
if (error) {
- toast.error(
- getReadableErrorMessage(
+ toast.add({
+ type: "error",
+ description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when verifying two-factor setup code fails",
message: "Failed to verify your code. Please try again.",
}),
),
- { id: toastId },
- );
+ id: toastId,
+ });
return;
}
- toast.dismiss(toastId);
+ toast.close(toastId);
setStep("backup");
},
});
@@ -131,7 +133,7 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
});
const onConfirmBackup = () => {
- toast.success(t`Two-factor authentication has been setup successfully.`);
+ toast.add({ type: "success", description: t`Two-factor authentication has been setup successfully.` });
void router.invalidate();
closeDialog();
onReset();
@@ -150,13 +152,13 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
const secret = extractSecretFromTotpUri(totpUri);
if (!secret) return;
await navigator.clipboard.writeText(secret);
- toast.success(t`Secret copied to clipboard.`);
+ toast.add({ type: "success", description: t`Secret copied to clipboard.` });
};
const handleCopyBackupCodes = async () => {
if (!backupCodes) return;
await navigator.clipboard.writeText(backupCodes.join("\n"));
- toast.success(t`Backup codes copied to clipboard.`);
+ toast.add({ type: "success", description: t`Backup codes copied to clipboard.` });
};
const handleDownloadBackupCodes = () => {
diff --git a/apps/web/src/dialogs/resume/import.tsx b/apps/web/src/dialogs/resume/import.tsx
index b13f68825..44c447af4 100644
--- a/apps/web/src/dialogs/resume/import.tsx
+++ b/apps/web/src/dialogs/resume/import.tsx
@@ -9,7 +9,6 @@ import { useStore } from "@tanstack/react-form";
import { useMutation } from "@tanstack/react-query";
import { Link, useNavigate } from "@tanstack/react-router";
import { useRef, useState } from "react";
-import { toast } from "sonner";
import z from "zod";
import { Badge } from "@reactive-resume/ui/components/badge";
import { Button } from "@reactive-resume/ui/components/button";
@@ -23,6 +22,7 @@ import {
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
import { Spinner } from "@reactive-resume/ui/components/spinner";
+import { toast } from "@reactive-resume/ui/components/toast";
import { Combobox } from "@/components/ui/combobox";
import { useHasUsableAiProvider } from "@/features/settings/integrations/hooks/use-has-usable-ai-provider";
import { useConfirm } from "@/hooks/use-confirm";
@@ -141,7 +141,9 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) {
setIsImporting(true);
- const toastId = toast.loading(t`Importing your resume...`, {
+ const toastId = toast.add({
+ type: "loading",
+ title: t`Importing your resume...`,
description: t`This may take a few minutes, depending on the response of the AI provider. Please do not close the window or refresh the page.`,
});
@@ -196,12 +198,19 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) {
}
const id = await importResume({ data });
- toast.success(t`Your resume has been imported successfully.`, { id: toastId, description: null });
+ toast.add({
+ type: "success",
+ title: null,
+ description: t`Your resume has been imported successfully.`,
+ id: toastId,
+ });
closeDialog();
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
} catch (error: unknown) {
- toast.error(
- getOrpcErrorMessage(error, {
+ toast.add({
+ type: "error",
+ title: null,
+ description: getOrpcErrorMessage(error, {
byCode: {
BAD_REQUEST: t({
comment: "Error shown when AI parsing returns invalid resume structure during import",
@@ -217,8 +226,8 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) {
message: "An unknown error occurred while importing your resume.",
}),
}),
- { id: toastId, description: null },
- );
+ id: toastId,
+ });
} finally {
setIsImporting(false);
}
diff --git a/apps/web/src/dialogs/resume/index.tsx b/apps/web/src/dialogs/resume/index.tsx
index 5bc5c8c44..266616537 100644
--- a/apps/web/src/dialogs/resume/index.tsx
+++ b/apps/web/src/dialogs/resume/index.tsx
@@ -7,7 +7,6 @@ import { useStore } from "@tanstack/react-form";
import { useMutation } from "@tanstack/react-query";
import { useNavigate, useParams } from "@tanstack/react-router";
import { useEffect, useRef } from "react";
-import { toast } from "sonner";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
import { ButtonGroup } from "@reactive-resume/ui/components/button-group";
@@ -32,6 +31,7 @@ import {
InputGroupInput,
InputGroupText,
} from "@reactive-resume/ui/components/input-group";
+import { toast } from "@reactive-resume/ui/components/toast";
import { generateId, generateRandomName, slugify } from "@reactive-resume/utils/string";
import { ChipInput } from "@/components/input/chip-input";
import { usePatchResume } from "@/features/resume/builder/draft";
@@ -75,17 +75,17 @@ export function CreateResumeDialog(_: DialogProps<"resume.create">) {
},
validators: { onSubmit: formSchema },
onSubmit: ({ value }) => {
- const toastId = toast.loading(t`Creating your resume...`);
+ const toastId = toast.add({ type: "loading", description: t`Creating your resume...` });
createResume(value, {
onSuccess: (id) => {
didCreateRef.current = true;
- toast.success(t`Your resume has been created successfully.`, { id: toastId });
+ toast.add({ type: "success", description: t`Your resume has been created successfully.`, id: toastId });
closeDialog();
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
},
onError: (error) => {
- toast.error(getResumeErrorMessage(error), { id: toastId });
+ toast.add({ type: "error", description: getResumeErrorMessage(error), id: toastId });
},
});
},
@@ -112,17 +112,17 @@ export function CreateResumeDialog(_: DialogProps<"resume.create">) {
withSampleData: true,
} satisfies RouterInput["resume"]["create"];
- const toastId = toast.loading(t`Creating your resume...`);
+ const toastId = toast.add({ type: "loading", description: t`Creating your resume...` });
createResume(data, {
onSuccess: (id) => {
didCreateRef.current = true;
- toast.success(t`Your resume has been created successfully.`, { id: toastId });
+ toast.add({ type: "success", description: t`Your resume has been created successfully.`, id: toastId });
closeDialog();
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
},
onError: (error) => {
- toast.error(getResumeErrorMessage(error), { id: toastId });
+ toast.add({ type: "error", description: getResumeErrorMessage(error), id: toastId });
},
});
};
@@ -200,7 +200,7 @@ export function UpdateResumeDialog({ data }: DialogProps<"resume.update">) {
},
validators: { onSubmit: formSchema },
onSubmit: ({ value }) => {
- const toastId = toast.loading(t`Updating your resume...`);
+ const toastId = toast.add({ type: "loading", description: t`Updating your resume...` });
updateResume(value, {
onSuccess: (updated) => {
@@ -215,11 +215,11 @@ export function UpdateResumeDialog({ data }: DialogProps<"resume.update">) {
});
}
- toast.success(t`Your resume has been updated successfully.`, { id: toastId });
+ toast.add({ type: "success", description: t`Your resume has been updated successfully.`, id: toastId });
closeDialog();
},
onError: (error) => {
- toast.error(getResumeErrorMessage(error), { id: toastId });
+ toast.add({ type: "error", description: getResumeErrorMessage(error), id: toastId });
},
});
},
@@ -281,18 +281,18 @@ export function DuplicateResumeDialog({ data }: DialogProps<"resume.duplicate">)
},
validators: { onSubmit: formSchema },
onSubmit: ({ value }) => {
- const toastId = toast.loading(t`Duplicating your resume...`);
+ const toastId = toast.add({ type: "loading", description: t`Duplicating your resume...` });
duplicateResume(value, {
onSuccess: (id) => {
- toast.success(t`Your resume has been duplicated successfully.`, { id: toastId });
+ toast.add({ type: "success", description: t`Your resume has been duplicated successfully.`, id: toastId });
closeDialog();
if (!data.shouldRedirect) return;
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
},
onError: (error) => {
- toast.error(getResumeErrorMessage(error), { id: toastId });
+ toast.add({ type: "error", description: getResumeErrorMessage(error), id: toastId });
},
});
},
diff --git a/apps/web/src/dialogs/resume/template/gallery.tsx b/apps/web/src/dialogs/resume/template/gallery.tsx
index a5beda4be..be7aaecd7 100644
--- a/apps/web/src/dialogs/resume/template/gallery.tsx
+++ b/apps/web/src/dialogs/resume/template/gallery.tsx
@@ -4,10 +4,10 @@ import type { TemplateMetadata } from "./data";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { SlideshowIcon } from "@phosphor-icons/react";
-import { toast } from "sonner";
import { Badge } from "@reactive-resume/ui/components/badge";
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@reactive-resume/ui/components/dialog";
import { ScrollArea } from "@reactive-resume/ui/components/scroll-area";
+import { toast } from "@reactive-resume/ui/components/toast";
import { cn } from "@reactive-resume/utils/style";
import { CometCard } from "@/components/animation/comet-card";
import { useDialogStore } from "@/dialogs/store";
@@ -33,9 +33,10 @@ export function TemplateGalleryDialog(_: DialogProps<"resume.template.gallery">)
closeDialog();
- toast(t`Switched to the ${templates[template].name} template.`, {
- action: {
- label: t`Undo`,
+ toast.add({
+ description: t`Switched to the ${templates[template].name} template.`,
+ actionProps: {
+ children: t`Undo`,
onClick: () => {
updateResumeData((draft) => {
draft.metadata.template = previousTemplate;
diff --git a/apps/web/src/features/applications/components/application-actions-menu.tsx b/apps/web/src/features/applications/components/application-actions-menu.tsx
index 204dea64f..f577a69c3 100644
--- a/apps/web/src/features/applications/components/application-actions-menu.tsx
+++ b/apps/web/src/features/applications/components/application-actions-menu.tsx
@@ -10,7 +10,6 @@ import {
TrayArrowUpIcon,
} from "@phosphor-icons/react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { toast } from "sonner";
import { STAGES } from "@reactive-resume/schema/applications/data";
import { Button } from "@reactive-resume/ui/components/button";
import {
@@ -23,6 +22,7 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@reactive-resume/ui/components/dropdown-menu";
+import { toast } from "@reactive-resume/ui/components/toast";
import { cn } from "@reactive-resume/utils/style";
import { useConfirm } from "@/hooks/use-confirm";
import { orpc } from "@/libs/orpc/client";
@@ -55,7 +55,7 @@ export function ApplicationActionsMenu({ application, onEdit, showOnHover, class
const update = useMutation(
orpc.applications.update.mutationOptions({
onSuccess: invalidate,
- onError: () => toast.error(t`Something went wrong. Please try again.`),
+ onError: () => toast.add({ type: "error", description: t`Something went wrong. Please try again.` }),
}),
);
@@ -63,9 +63,9 @@ export function ApplicationActionsMenu({ application, onEdit, showOnHover, class
orpc.applications.delete.mutationOptions({
onSuccess: () => {
invalidate();
- toast.success(t`Application deleted.`);
+ toast.add({ type: "success", description: t`Application deleted.` });
},
- onError: () => toast.error(t`Couldn't delete the application.`),
+ onError: () => toast.add({ type: "error", description: t`Couldn't delete the application.` }),
}),
);
diff --git a/apps/web/src/features/applications/components/application-ai-copilot.tsx b/apps/web/src/features/applications/components/application-ai-copilot.tsx
index 8c145b569..12e2839b8 100644
--- a/apps/web/src/features/applications/components/application-ai-copilot.tsx
+++ b/apps/web/src/features/applications/components/application-ai-copilot.tsx
@@ -13,7 +13,7 @@ import {
} from "@phosphor-icons/react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
-import { toast } from "sonner";
+import { toast } from "@reactive-resume/ui/components/toast";
import { cn } from "@reactive-resume/utils/style";
import { orpc } from "@/libs/orpc/client";
import { applicationsListQueryKey } from "../queries";
@@ -113,22 +113,22 @@ export function ApplicationAiCopilot({ application }: Props) {
const matchScore = useMutation(
orpc.applications.ai.matchScore.mutationOptions({
onSuccess: invalidate,
- onError: (error) => toast.error(error.message || t`Match scoring failed.`),
+ onError: (error) => toast.add({ type: "error", description: error.message || t`Match scoring failed.` }),
}),
);
const tailorResume = useMutation(
orpc.applications.ai.tailorResume.mutationOptions({
onSuccess: (result) => {
invalidate();
- toast.success(t`Created "${result.name}" and linked it to this application.`);
+ toast.add({ type: "success", description: t`Created "${result.name}" and linked it to this application.` });
},
- onError: (error) => toast.error(error.message || t`Tailoring failed.`),
+ onError: (error) => toast.add({ type: "error", description: error.message || t`Tailoring failed.` }),
}),
);
const draftMessage = useMutation(
orpc.applications.ai.draftMessage.mutationOptions({
onSuccess: (result, variables) => setDraft({ kind: variables.kind, text: result.text }),
- onError: (error) => toast.error(error.message || t`Drafting failed.`),
+ onError: (error) => toast.add({ type: "error", description: error.message || t`Drafting failed.` }),
}),
);
@@ -152,7 +152,7 @@ export function ApplicationAiCopilot({ application }: Props) {
{!canScore ? (
- Link a resume and add a job description (Edit) to score your fit and tailor a copy.
+ Link a resume and paste the job description (Edit) to score your fit and tailor a copy.