refactor(web): move toast call sites to the new component

Swaps sonner's toast.success/error/loading/dismiss for the new toast.add({ type,
description }) and toast.close across dialogs, auth pages, the builder, the
dashboard and the applications views. Behaviour is unchanged.
This commit is contained in:
Amruth Pillai
2026-08-17 22:32:32 +02:00
parent 170550ed59
commit 23ceee2148
51 changed files with 472 additions and 431 deletions
+4 -2
View File
@@ -38,7 +38,6 @@ import { TextStyle } from "@tiptap/extension-text-style";
import { EditorContent, EditorContext, useEditor, useEditorState } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { match } from "ts-pattern";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
@@ -51,6 +50,7 @@ import {
DropdownMenuTrigger,
} from "@reactive-resume/ui/components/dropdown-menu";
import { PopoverHeader, PopoverTitle, PopoverTrigger } from "@reactive-resume/ui/components/popover";
import { toast } from "@reactive-resume/ui/components/toast";
import { Toggle } from "@reactive-resume/ui/components/toggle";
import { isDarkColor } from "@reactive-resume/utils/color";
import { cn } from "@reactive-resume/utils/style";
@@ -319,7 +319,9 @@ function useEditorToolbarState(editor: Editor) {
}
if (!z.url({ protocol: /^https?$/ }).safeParse(url).success) {
toast.error(t`The URL you entered is not valid.`, {
toast.add({
type: "error",
title: t`The URL you entered is not valid.`,
description: t`Valid URLs must start with http:// or https://.`,
});
return;
@@ -1,17 +1,17 @@
// @vitest-environment happy-dom
import type React from "react";
import { act, fireEvent, render, screen } from "@testing-library/react";
import { act, render } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { DonationToast } from "./donation-toast";
type ToastOptions = {
dismissible: boolean;
duration: number;
type AddOptions = {
actionProps: { children: string; onClick: () => void };
description: string;
id: string;
unstyled: boolean;
onClose: () => void;
timeout: number;
title: string;
};
const cookieMock = vi.hoisted(() => ({
@@ -21,8 +21,8 @@ const cookieMock = vi.hoisted(() => ({
const toastMock = vi.hoisted(() => ({
toast: {
custom: vi.fn(),
dismiss: vi.fn(),
add: vi.fn(),
close: vi.fn(),
},
}));
@@ -33,22 +33,18 @@ vi.mock("js-cookie", () => ({
},
}));
vi.mock("sonner", () => ({
vi.mock("@reactive-resume/ui/components/toast", () => ({
toast: toastMock.toast,
}));
const getCustomToast = () =>
toastMock.toast.custom.mock.calls[0] as [(toastId: string | number) => React.ReactElement, ToastOptions] | undefined;
const getAddOptions = () => {
const call = toastMock.toast.add.mock.calls[0] as [AddOptions] | undefined;
if (!call) throw new Error("Donation toast was not shown.");
return call[0];
};
const SHOW_TOAST_DELAY_MS = 5 * 60 * 1000;
const renderCustomToast = () => {
const customToast = getCustomToast();
if (!customToast) throw new Error("Custom toast was not rendered.");
return render(<I18nProvider i18n={i18n}>{customToast[0]("donation-toast")}</I18nProvider>);
};
describe("DonationToast", () => {
beforeEach(() => {
vi.useFakeTimers();
@@ -56,8 +52,8 @@ describe("DonationToast", () => {
i18n.loadAndActivate({ locale: "en-US", messages: {} });
cookieMock.value = null;
cookieMock.set.mockClear();
toastMock.toast.custom.mockClear();
toastMock.toast.dismiss.mockClear();
toastMock.toast.add.mockClear();
toastMock.toast.close.mockClear();
vi.spyOn(window, "open").mockReturnValue(null);
});
@@ -69,24 +65,22 @@ describe("DonationToast", () => {
it("waits before showing the donation toast", () => {
render(<DonationToast />);
expect(toastMock.toast.custom).not.toHaveBeenCalled();
expect(toastMock.toast.add).not.toHaveBeenCalled();
act(() => {
vi.advanceTimersByTime(SHOW_TOAST_DELAY_MS - 1);
});
expect(toastMock.toast.custom).not.toHaveBeenCalled();
expect(toastMock.toast.add).not.toHaveBeenCalled();
act(() => {
vi.advanceTimersByTime(1);
});
expect(toastMock.toast.custom).toHaveBeenCalledWith(
expect.any(Function),
expect(toastMock.toast.add).toHaveBeenCalledWith(
expect.objectContaining({
dismissible: false,
duration: Number.POSITIVE_INFINITY,
id: "donation-toast",
unstyled: true,
timeout: 0,
title: "Please support the project",
}),
);
});
@@ -100,18 +94,19 @@ describe("DonationToast", () => {
vi.advanceTimersByTime(SHOW_TOAST_DELAY_MS);
});
expect(toastMock.toast.custom).not.toHaveBeenCalled();
expect(toastMock.toast.add).not.toHaveBeenCalled();
});
it("sets a 30-day dismissed cookie when dismissed", () => {
it("sets a 30-day dismissed cookie when closed", () => {
render(<DonationToast />);
act(() => {
vi.advanceTimersByTime(SHOW_TOAST_DELAY_MS);
});
renderCustomToast();
fireEvent.click(screen.getByRole("button", { name: "Dismiss" }));
act(() => {
getAddOptions().onClose();
});
expect(cookieMock.set).toHaveBeenCalledWith("donation-toast-dismissed", "true", {
path: "/",
@@ -119,30 +114,27 @@ describe("DonationToast", () => {
sameSite: "lax",
expires: new Date("2026-06-10T12:05:00.000Z"),
});
expect(toastMock.toast.dismiss).toHaveBeenCalledWith("donation-toast");
});
it("sets a 30-day dismissed cookie and opens Open Collective when donated", () => {
it("opens Open Collective and closes the toast when donating", () => {
render(<DonationToast />);
act(() => {
vi.advanceTimersByTime(SHOW_TOAST_DELAY_MS);
});
renderCustomToast();
fireEvent.click(screen.getByRole("button", { name: "Donate" }));
const options = getAddOptions();
expect(options.actionProps.children).toBe("Donate");
expect(cookieMock.set).toHaveBeenCalledWith("donation-toast-dismissed", "true", {
path: "/",
secure: true,
sameSite: "lax",
expires: new Date("2026-06-10T12:05:00.000Z"),
act(() => {
options.actionProps.onClick();
});
expect(window.open).toHaveBeenCalledWith(
"https://opencollective.com/reactive-resume/donate",
"_blank",
"noopener,noreferrer",
);
expect(toastMock.toast.dismiss).toHaveBeenCalledWith("donation-toast");
expect(toastMock.toast.close).toHaveBeenCalledWith("donation-toast");
});
});
+17 -54
View File
@@ -1,10 +1,8 @@
import { Trans } from "@lingui/react/macro";
import { HandHeartIcon } from "@phosphor-icons/react";
import { t } from "@lingui/core/macro";
import Cookies from "js-cookie";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import { useTimeout } from "usehooks-ts";
import { Button } from "@reactive-resume/ui/components/button";
import { toast } from "@reactive-resume/ui/components/toast";
const TOAST_ID = "donation-toast";
const SHOW_TOAST_DELAY_MS = 5 * 60 * 1000; // 5 minutes
@@ -26,22 +24,22 @@ export function DonationToast() {
const showToast = useCallback(() => {
if (dismissed === "true") return;
const onDonate = (t: string | number) => {
toast.dismiss(t);
setDismissed("true", { expires: getDismissedCookieExpiresAt() });
window.open("https://opencollective.com/reactive-resume/donate", "_blank", "noopener,noreferrer");
};
const onDismiss = (t: string | number) => {
toast.dismiss(t);
setDismissed("true", { expires: getDismissedCookieExpiresAt() });
};
toast.custom((t) => <DonationToastCard onDismiss={() => onDismiss(t)} onDonate={() => onDonate(t)} />, {
toast.add({
id: TOAST_ID,
unstyled: true,
dismissible: false,
duration: Number.POSITIVE_INFINITY,
// Never auto-dismisses: closing it is what records the 30-day cookie.
timeout: 0,
title: t`Please support the project`,
description: t`Reactive Resume is free and open source. If it has helped you, please consider donating.`,
actionProps: {
children: t`Donate`,
onClick: () => {
window.open("https://opencollective.com/reactive-resume/donate", "_blank", "noopener,noreferrer");
toast.close(TOAST_ID);
},
},
onClose: () => {
setDismissed("true", { expires: getDismissedCookieExpiresAt() });
},
});
}, [dismissed, setDismissed]);
@@ -49,38 +47,3 @@ export function DonationToast() {
return null;
}
type DonationToastCardProps = {
onDismiss: () => void;
onDonate: () => void;
};
function DonationToastCard({ onDismiss, onDonate }: DonationToastCardProps) {
return (
<div className="w-sm rounded-md bg-popover p-4 shadow-xl">
<div className="flex items-start gap-3">
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-amber-300 text-amber-950">
<HandHeartIcon aria-hidden="true" />
</div>
<div className="min-w-0 flex-1 space-y-1">
<p className="font-semibold text-sm tracking-tight">
<Trans>Please support the project</Trans>
</p>
<p className="text-pretty text-muted-foreground text-xs">
<Trans>Reactive Resume is free and open source. If it has helped you, please consider donating.</Trans>
</p>
</div>
</div>
<div className="mt-4 grid grid-cols-2 gap-2">
<Button size="sm" variant="outline" onClick={onDismiss}>
<Trans>Dismiss</Trans>
</Button>
<Button size="sm" onClick={onDonate} className="bg-amber-300 text-amber-950 hover:bg-amber-200">
<Trans>Donate</Trans>
</Button>
</div>
</div>
);
}
+9 -8
View File
@@ -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 = () => {
@@ -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();
},
@@ -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();
+19 -17
View File
@@ -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 = () => {
+16 -7
View File
@@ -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);
}
+13 -13
View File
@@ -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 });
},
});
},
@@ -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;
@@ -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.` }),
}),
);
@@ -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) {
<div className="px-3.5 py-3">
{!canScore ? (
<p className="rounded-lg bg-muted/50 p-2.5 text-muted-foreground text-xs">
<Trans>Link a resume and add a job description (Edit) to score your fit and tailor a copy.</Trans>
<Trans>Link a resume and paste the job description (Edit) to score your fit and tailor a copy.</Trans>
</p>
) : score == null ? (
<button
@@ -245,7 +245,7 @@ export function ApplicationAiCopilot({ application }: Props) {
className="inline-flex items-center gap-1 text-muted-foreground text-xs hover:text-foreground"
onClick={() => {
void navigator.clipboard.writeText(draft.text);
toast.success(t`Copied to clipboard.`);
toast.add({ type: "success", description: t`Copied to clipboard.` });
}}
>
<CopyIcon className="size-3.5" /> <Trans>Copy</Trans>
@@ -14,7 +14,6 @@ import {
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
import { STAGES } from "@reactive-resume/schema/applications/data";
import { Button } from "@reactive-resume/ui/components/button";
import {
@@ -28,6 +27,7 @@ import {
import { Input } from "@reactive-resume/ui/components/input";
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@reactive-resume/ui/components/sheet";
import { Textarea } from "@reactive-resume/ui/components/textarea";
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";
@@ -83,28 +83,28 @@ export function ApplicationDetailSheet({ application, onOpenChange, onEdit }: Pr
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.` }),
}),
);
const addNote = useMutation(
orpc.applications.addNote.mutationOptions({
onSuccess: invalidate,
onError: () => toast.error(t`Couldn't save the note.`),
onError: () => toast.add({ type: "error", description: t`Couldn't save the note.` }),
}),
);
const updateTimelineEntry = useMutation(
orpc.applications.updateTimelineEntry.mutationOptions({
onSuccess: invalidate,
onError: () => toast.error(t`Couldn't update the timeline entry.`),
onError: () => toast.add({ type: "error", description: t`Couldn't update the timeline entry.` }),
}),
);
const deleteTimelineEntry = useMutation(
orpc.applications.deleteTimelineEntry.mutationOptions({
onSuccess: invalidate,
onError: () => toast.error(t`Couldn't delete the timeline entry.`),
onError: () => toast.add({ type: "error", description: t`Couldn't delete the timeline entry.` }),
}),
);
@@ -112,10 +112,10 @@ export function ApplicationDetailSheet({ application, onOpenChange, onEdit }: Pr
orpc.applications.delete.mutationOptions({
onSuccess: () => {
invalidate();
toast.success(t`Application deleted.`);
toast.add({ type: "success", description: t`Application deleted.` });
onOpenChange(false);
},
onError: () => toast.error(t`Couldn't delete the application.`),
onError: () => toast.add({ type: "error", description: t`Couldn't delete the application.` }),
}),
);
@@ -14,8 +14,8 @@ import {
import { t } from "@lingui/core/macro";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { STAGES } from "@reactive-resume/schema/applications/data";
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";
@@ -48,7 +48,7 @@ export function ApplicationBoard({ applications, onOpen, onEdit }: Props) {
},
onError: (_error, _vars, context) => {
if (context?.previous) queryClient.setQueryData(listKey, context.previous);
toast.error(t`Couldn't move the application. Please try again.`);
toast.add({ type: "error", description: t`Couldn't move the application. Please try again.` });
},
onSettled: () => void queryClient.invalidateQueries({ queryKey: listKey }),
}),
@@ -3,7 +3,7 @@ import { Trans } from "@lingui/react/macro";
import { FilePdfIcon, UploadSimpleIcon, XIcon } from "@phosphor-icons/react";
import { useMutation } from "@tanstack/react-query";
import { useRef } from "react";
import { toast } from "sonner";
import { toast } from "@reactive-resume/ui/components/toast";
import { orpc } from "@/libs/orpc/client";
export type FileAttachment = { url: string; name: string };
@@ -28,17 +28,18 @@ export function FileAttachmentField({ value, onChange, attachLabel, disabled }:
const file = event.target.files?.[0];
if (!file) return;
if (file.type !== "application/pdf") {
toast.error(t`Please upload a PDF file.`);
toast.add({ type: "error", description: t`Please upload a PDF file.` });
return;
}
const toastId = toast.loading(t`Uploading…`);
const toastId = toast.add({ type: "loading", description: t`Uploading…` });
upload.mutate(file, {
onSuccess: ({ url }) => {
toast.dismiss(toastId);
toast.close(toastId);
onChange({ url, name: file.name });
if (inputRef.current) inputRef.current.value = "";
},
onError: () => toast.error(t`Couldn't upload the file. Please try again.`, { id: toastId }),
onError: () =>
toast.add({ type: "error", description: t`Couldn't upload the file. Please try again.`, id: toastId }),
});
};
@@ -3,7 +3,6 @@ import { Trans } from "@lingui/react/macro";
import { CheckCircleIcon, UploadSimpleIcon } from "@phosphor-icons/react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { Badge } from "@reactive-resume/ui/components/badge";
import { Button } from "@reactive-resume/ui/components/button";
import { Label } from "@reactive-resume/ui/components/label";
@@ -16,6 +15,7 @@ import {
SheetTitle,
} from "@reactive-resume/ui/components/sheet";
import { Textarea } from "@reactive-resume/ui/components/textarea";
import { toast } from "@reactive-resume/ui/components/toast";
import { orpc } from "@/libs/orpc/client";
import { mapCsvToApplications, parseCsv } from "../csv";
import { applicationsListQueryKey } from "../queries";
@@ -51,12 +51,12 @@ export function ImportApplicationsSheet({ open, onOpenChange }: Props) {
void queryClient.invalidateQueries({ queryKey: applicationsListQueryKey() });
void queryClient.invalidateQueries({ queryKey: orpc.applications.stats.queryKey() });
void queryClient.invalidateQueries({ queryKey: orpc.applications.tags.queryKey() });
toast.success(t`Imported ${result.imported} application(s).`);
toast.add({ type: "success", description: t`Imported ${result.imported} application(s).` });
setText("");
resetFile();
onOpenChange(false);
},
onError: () => toast.error(t`Import failed. Check the CSV and try again.`),
onError: () => toast.add({ type: "error", description: t`Import failed. Check the CSV and try again.` }),
}),
);
@@ -5,8 +5,8 @@ import { Trans } from "@lingui/react/macro";
import { DownloadSimpleIcon } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { useMemo, useRef } from "react";
import { toast } from "sonner";
import { Button } from "@reactive-resume/ui/components/button";
import { toast } from "@reactive-resume/ui/components/toast";
import { orpc } from "@/libs/orpc/client";
import { computeInsights, computeTimeline } from "../insights";
@@ -223,7 +223,7 @@ function PipelineFlow({ insights }: { insights: ReturnType<typeof computeInsight
link.download = "pipeline-flow.png";
link.href = canvas.toDataURL("image/png");
link.click();
toast.success(t`Exported pipeline-flow.png`);
toast.add({ type: "success", description: t`Exported pipeline-flow.png` });
};
image.src = svg64;
};
@@ -5,7 +5,6 @@ import { Trans } from "@lingui/react/macro";
import { ArchiveIcon, ArrowRightIcon, TagIcon, TrashIcon } from "@phosphor-icons/react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { STAGES } from "@reactive-resume/schema/applications/data";
import { Badge } from "@reactive-resume/ui/components/badge";
import { Button } from "@reactive-resume/ui/components/button";
@@ -18,6 +17,7 @@ import {
} from "@reactive-resume/ui/components/dropdown-menu";
import { Input } from "@reactive-resume/ui/components/input";
import { Popover, PopoverContent, PopoverTrigger } from "@reactive-resume/ui/components/popover";
import { toast } from "@reactive-resume/ui/components/toast";
import { getInitials } from "@reactive-resume/utils/string";
import { cn } from "@reactive-resume/utils/style";
import { orpc } from "@/libs/orpc/client";
@@ -70,7 +70,7 @@ export function ApplicationTable({ applications, onOpen, onEdit }: Props) {
invalidate();
clearSelection();
},
onError: () => toast.error(t`Bulk update failed. Please try again.`),
onError: () => toast.add({ type: "error", description: t`Bulk update failed. Please try again.` }),
}),
);
@@ -79,9 +79,9 @@ export function ApplicationTable({ applications, onOpen, onEdit }: Props) {
onSuccess: (result) => {
invalidate();
clearSelection();
toast.success(t`Deleted ${result.deleted} application(s).`);
toast.add({ type: "success", description: t`Deleted ${result.deleted} application(s).` });
},
onError: () => toast.error(t`Bulk delete failed. Please try again.`),
onError: () => toast.add({ type: "error", description: t`Bulk delete failed. Please try again.` }),
}),
);
@@ -4,9 +4,9 @@ import { Trans } from "@lingui/react/macro";
import { FingerprintIcon, GithubLogoIcon, GoogleLogoIcon, LinkedinLogoIcon, VaultIcon } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { useRouter } from "@tanstack/react-router";
import { toast } from "sonner";
import { Button } from "@reactive-resume/ui/components/button";
import { Skeleton } from "@reactive-resume/ui/components/skeleton";
import { toast } from "@reactive-resume/ui/components/toast";
import { cn } from "@reactive-resume/utils/style";
import { authClient } from "@/libs/auth/client";
import { orpc } from "@/libs/orpc/client";
@@ -50,20 +50,22 @@ function SocialAuthButtons({ providers }: SocialAuthButtonsProps) {
const router = useRouter();
const runSignIn = async (fn: () => Promise<{ error: { message?: string } | null }>) => {
const toastId = toast.loading(t`Signing in...`);
const toastId = toast.add({ type: "loading", description: t`Signing in...` });
const { error } = await fn();
if (error) {
toast.error(
error.message ||
toast.add({
type: "error",
description:
error.message ||
t({
comment: "Fallback toast when sign-in fails without an error message",
message: "Failed to sign in. Please try again.",
}),
{ id: toastId },
);
id: toastId,
});
return;
}
toast.dismiss(toastId);
toast.close(toastId);
await router.invalidate();
};
@@ -3,11 +3,11 @@ import { Trans } from "@lingui/react/macro";
import { ArrowRightIcon } from "@phosphor-icons/react";
import { Link } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
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 { authClient } from "@/libs/auth/client";
import { useAppForm } from "@/libs/tanstack-form";
@@ -22,7 +22,7 @@ export function ForgotPasswordPage() {
defaultValues: { email: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
const toastId = toast.loading(t`Sending password reset email...`);
const toastId = toast.add({ type: "loading", description: t`Sending password reset email...` });
const { error } = await authClient.requestPasswordReset({
email: value.email,
@@ -30,19 +30,21 @@ export function ForgotPasswordPage() {
});
if (error) {
toast.error(
error.message ||
toast.add({
type: "error",
description:
error.message ||
t({
comment: "Fallback toast when requesting password reset email fails without backend message",
message: "Failed to send password reset email. Please try again.",
}),
{ id: toastId },
);
id: toastId,
});
return;
}
setSubmitted(true);
toast.dismiss(toastId);
toast.close(toastId);
},
});
+11 -9
View File
@@ -4,12 +4,12 @@ import { ArrowRightIcon, EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { Link, useNavigate, useRouter } from "@tanstack/react-router";
import { useEffect, useRef } from "react";
import { toast } from "sonner";
import { useToggle } from "usehooks-ts";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
import { FormControl, FormDescription, 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 { authClient } from "@/libs/auth/client";
import { orpc } from "@/libs/orpc/client";
import { useAppForm } from "@/libs/tanstack-form";
@@ -38,7 +38,7 @@ export function LoginPage({ disableEmailAuth, disableSignups }: Props) {
defaultValues: { identifier: "", password: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
const toastId = toast.loading(t`Signing in...`);
const toastId = toast.add({ type: "loading", description: t`Signing in...` });
try {
const isEmail = value.identifier.includes("@");
@@ -48,14 +48,16 @@ export function LoginPage({ disableEmailAuth, disableSignups }: Props) {
: await authClient.signIn.username({ username: value.identifier, password: value.password });
if (result.error) {
toast.error(
result.error.message ||
toast.add({
type: "error",
description:
result.error.message ||
t({
comment: "Fallback toast when sign-in fails and no server error message is available",
message: "Failed to sign in. Please try again.",
}),
{ id: toastId },
);
id: toastId,
});
return;
}
@@ -66,16 +68,16 @@ export function LoginPage({ disableEmailAuth, disableSignups }: Props) {
result.data.twoFactorRedirect;
if (requiresTwoFactor) {
toast.dismiss(toastId);
toast.close(toastId);
void navigate({ to: "/auth/verify-2fa", replace: true });
return;
}
toast.dismiss(toastId);
toast.close(toastId);
await router.invalidate();
void navigate({ to: "/dashboard", replace: true });
} catch {
toast.error(t`Failed to sign in. Please try again.`, { id: toastId });
toast.add({ type: "error", description: t`Failed to sign in. Please try again.`, id: toastId });
}
},
});
@@ -3,13 +3,13 @@ import { Trans } from "@lingui/react/macro";
import { ArrowRightIcon, EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
import { Link } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
import { useToggle } from "usehooks-ts";
import z from "zod";
import { Alert, AlertDescription, AlertTitle } from "@reactive-resume/ui/components/alert";
import { Button } from "@reactive-resume/ui/components/button";
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 { authClient } from "@/libs/auth/client";
import { useAppForm } from "@/libs/tanstack-form";
import { SocialAuth } from "../components/social-auth";
@@ -41,7 +41,7 @@ export function RegisterPage({ disableEmailAuth }: Props) {
defaultValues: { name: "", username: "", email: "", password: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
const toastId = toast.loading(t`Signing up...`);
const toastId = toast.add({ type: "loading", description: t`Signing up...` });
const { error } = await authClient.signUp.email({
name: value.name,
@@ -53,19 +53,21 @@ export function RegisterPage({ disableEmailAuth }: Props) {
});
if (error) {
toast.error(
error.message ||
toast.add({
type: "error",
description:
error.message ||
t({
comment: "Fallback toast when account registration fails without a server error message",
message: "Failed to create your account. Please try again.",
}),
{ id: toastId },
);
id: toastId,
});
return;
}
setSubmitted(true);
toast.dismiss(toastId);
toast.close(toastId);
},
});
@@ -2,12 +2,12 @@ import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
import { useNavigate } 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";
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 { authClient } from "@/libs/auth/client";
import { useAppForm } from "@/libs/tanstack-form";
@@ -27,23 +27,27 @@ export function ResetPasswordPage({ token }: Props) {
defaultValues: { password: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
const toastId = toast.loading(t`Resetting your password...`);
const toastId = toast.add({ type: "loading", description: t`Resetting your password...` });
const { error } = await authClient.resetPassword({ token, newPassword: value.password });
if (error) {
toast.error(
error.message ||
toast.add({
type: "error",
description:
error.message ||
t({
comment: "Fallback toast when resetting password fails and no backend message is available",
message: "Failed to reset your password. Please try again.",
}),
{ id: toastId },
);
id: toastId,
});
return;
}
toast.success(t`Your password has been reset successfully. You can now sign in with your new password.`, {
toast.add({
type: "success",
description: t`Your password has been reset successfully. You can now sign in with your new password.`,
id: toastId,
});
@@ -4,12 +4,12 @@ import { ORPCError } from "@orpc/client";
import { EyeIcon, EyeSlashIcon, LockOpenIcon } from "@phosphor-icons/react";
import { useMutation } from "@tanstack/react-query";
import { useNavigate } 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";
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 { getReadableErrorMessage } from "@/libs/error-message";
import { orpc } from "@/libs/orpc/client";
import { useAppForm } from "@/libs/tanstack-form";
@@ -35,18 +35,18 @@ export function ResumePasswordPage({ redirectPath }: Props) {
defaultValues: { password: "" },
validators: { onSubmit: formSchema },
onSubmit: ({ value, formApi }) => {
const toastId = toast.loading(t`Verifying password...`);
const toastId = toast.add({ type: "loading", description: t`Verifying password...` });
verifyPassword(
{ username, slug, password: value.password },
{
onSuccess: () => {
toast.dismiss(toastId);
toast.close(toastId);
void navigate({ to: redirectPath, replace: true });
},
onError: (error) => {
if (error instanceof ORPCError && error.code === "INVALID_PASSWORD") {
toast.dismiss(toastId);
toast.close(toastId);
formApi.setFieldMeta("password", (meta) => ({
...meta,
isTouched: true,
@@ -57,16 +57,17 @@ export function ResumePasswordPage({ redirectPath }: Props) {
},
}));
} else {
toast.error(
getReadableErrorMessage(
toast.add({
type: "error",
description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when resume password verification fails unexpectedly",
message: "Failed to verify the password. Please try again.",
}),
),
{ id: toastId },
);
id: toastId,
});
}
},
},
@@ -2,11 +2,11 @@ import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { ArrowLeftIcon, CheckIcon } from "@phosphor-icons/react";
import { Link, useNavigate, useRouter } from "@tanstack/react-router";
import { toast } from "sonner";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
import { FormControl, FormItem, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
import { toast } from "@reactive-resume/ui/components/toast";
import { authClient } from "@/libs/auth/client";
import { useAppForm } from "@/libs/tanstack-form";
@@ -30,15 +30,20 @@ function TwoFactorVerificationPage({ backupCode = false }: TwoFactorVerification
defaultValues: { code: "" },
validators: { onSubmit: backupCode ? backupCodeSchema : totpSchema },
onSubmit: async ({ value }) => {
const toastId = toast.loading(backupCode ? t`Verifying backup code...` : t`Verifying code...`);
const toastId = toast.add({
type: "loading",
description: backupCode ? t`Verifying backup code...` : t`Verifying code...`,
});
const code = backupCode ? `${value.code.slice(0, 5)}-${value.code.slice(5)}` : value.code;
const { error } = backupCode
? await authClient.twoFactor.verifyBackupCode({ code })
: await authClient.twoFactor.verifyTotp({ code });
if (error) {
toast.error(
error.message ||
toast.add({
type: "error",
description:
error.message ||
(backupCode
? t({
comment: "Fallback toast when verifying a backup two-factor authentication code fails",
@@ -48,12 +53,12 @@ function TwoFactorVerificationPage({ backupCode = false }: TwoFactorVerification
comment: "Fallback toast when verifying a two-factor authentication code fails",
message: "Failed to verify your code. Please try again.",
})),
{ id: toastId },
);
id: toastId,
});
return;
}
toast.dismiss(toastId);
toast.close(toastId);
await router.invalidate();
void navigate({ to: "/dashboard", replace: true });
},
@@ -31,8 +31,8 @@ const routerParamsMock = vi.hoisted(() => ({
}));
const toastMocks = vi.hoisted(() => ({
dismiss: vi.fn(),
error: vi.fn(() => "sync-error-toast"),
add: vi.fn(() => "sync-error-toast"),
close: vi.fn(),
}));
vi.mock("@orpc/client", () => ({
@@ -73,7 +73,7 @@ vi.mock("@/libs/orpc/client", () => ({
},
}));
vi.mock("sonner", () => ({
vi.mock("@reactive-resume/ui/components/toast", () => ({
toast: toastMocks,
}));
@@ -125,8 +125,8 @@ describe("builder resume autosave", () => {
queryClientMock.setQueryData.mockClear();
routerParamsMock.value = {};
i18n.loadAndActivate({ locale: "en-US", messages: {} });
toastMocks.dismiss.mockClear();
toastMocks.error.mockClear();
toastMocks.add.mockClear();
toastMocks.close.mockClear();
useResumeStore.getState().reset();
});
@@ -268,9 +268,8 @@ describe("builder resume autosave", () => {
await flushMicrotasks();
expect(useResumeStore.getState().resume?.data.basics.name).toBe("Unsaved Name");
expect(toastMocks.error).toHaveBeenCalledWith(
"Your latest changes could not be saved.",
expect.objectContaining({ duration: Number.POSITIVE_INFINITY }),
expect(toastMocks.add).toHaveBeenCalledWith(
expect.objectContaining({ type: "error", description: "Your latest changes could not be saved.", timeout: 0 }),
);
expect(orpcMocks.patchResume).not.toHaveBeenCalled();
});
+17 -9
View File
@@ -7,9 +7,9 @@ import { useQueryClient } from "@tanstack/react-query";
import { useParams } from "@tanstack/react-router";
import { debounce, isEqual } from "es-toolkit";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { immer } from "zustand/middleware/immer";
import { create } from "zustand/react";
import { toast } from "@reactive-resume/ui/components/toast";
import { orpc, streamClient } from "@/libs/orpc/client";
export type Resume = {
@@ -63,7 +63,7 @@ type Runtime = {
hasPendingLocalChanges: boolean;
isSaving: boolean;
pendingResume?: Resume;
syncErrorToastId?: string | number;
syncErrorToastId?: string;
syncResume: ReturnType<typeof debounce<(resume: Resume) => void>>;
beforeUnloadHandler?: () => void;
deferredRemoteResume?: Resume;
@@ -92,7 +92,7 @@ function resetHistoryRuntime() {
historyCanCoalesce = false;
}
let lockedToastId: string | number | undefined;
let lockedToastId: string | undefined;
function getResumeQueryKey(id: string): QueryKey {
return orpc.resume.getById.queryOptions({ input: { id } }).queryKey as QueryKey;
@@ -126,7 +126,7 @@ function externalUpdateMessage(mutation: ResumeUpdateMutation): string {
}
function notifyExternalUpdate(mutation: ResumeUpdateMutation) {
toast.info(externalUpdateMessage(mutation), { id: "resume-external-update" });
toast.add({ type: "info", description: externalUpdateMessage(mutation), id: "resume-external-update" });
}
// #54: applies a remote update that was deferred because the user was typing.
@@ -214,7 +214,7 @@ async function flushResumeSave(id: string) {
}
if (runtime.syncErrorToastId !== undefined) {
toast.dismiss(runtime.syncErrorToastId);
toast.close(runtime.syncErrorToastId);
runtime.syncErrorToastId = undefined;
}
} catch (error: unknown) {
@@ -223,9 +223,11 @@ async function flushResumeSave(id: string) {
runtime.pendingResume ??= submitted;
runtime.hasPendingLocalChanges = true;
useResumeStore.getState().setSaveStatus("error");
runtime.syncErrorToastId = toast.error(t`Your latest changes could not be saved.`, {
runtime.syncErrorToastId = toast.add({
type: "error",
description: t`Your latest changes could not be saved.`,
id: runtime.syncErrorToastId,
duration: Number.POSITIVE_INFINITY,
timeout: 0,
});
} finally {
runtime.isSaving = false;
@@ -417,7 +419,9 @@ export const useResumeStore = create<ResumeStore>()(
if (!currentResume) return;
if (currentResume.isLocked) {
lockedToastId = toast.error(t`This resume is locked and cannot be updated.`, {
lockedToastId = toast.add({
type: "error",
description: t`This resume is locked and cannot be updated.`,
id: lockedToastId,
});
return;
@@ -472,7 +476,11 @@ function applyHistoryStep(get: StoreGet, set: ImmerSet, direction: "undo" | "red
if (!currentResume) return;
if (currentResume.isLocked) {
lockedToastId = toast.error(t`This resume is locked and cannot be updated.`, { id: lockedToastId });
lockedToastId = toast.add({
type: "error",
description: t`This resume is locked and cannot be updated.`,
id: lockedToastId,
});
return;
}
@@ -10,7 +10,7 @@ const mocks = vi.hoisted(() => ({
createResumePdfBlob: vi.fn(async () => new Blob(["local"], { type: "application/pdf" })),
downloadWithAnchor: vi.fn(),
fetch: vi.fn(async (_input: string | URL) => new Response(new Blob(["server"], { type: "application/pdf" }))),
toastError: vi.fn(),
toastAdd: vi.fn(() => "toast"),
}));
vi.mock("@/features/resume/export/pdf-document", () => ({
@@ -20,11 +20,10 @@ vi.mock("@reactive-resume/utils/file", () => ({
downloadWithAnchor: mocks.downloadWithAnchor,
generateFilename: (name: string, extension: string) => `${name}.${extension}`,
}));
vi.mock("sonner", () => ({
vi.mock("@reactive-resume/ui/components/toast", () => ({
toast: {
loading: vi.fn(() => "toast"),
error: mocks.toastError,
dismiss: vi.fn(),
add: mocks.toastAdd,
close: vi.fn(),
},
}));
@@ -34,7 +33,7 @@ beforeEach(() => {
mocks.createResumePdfBlob.mockClear();
mocks.downloadWithAnchor.mockClear();
mocks.fetch.mockClear();
mocks.toastError.mockClear();
mocks.toastAdd.mockClear();
vi.stubGlobal("fetch", mocks.fetch);
});
@@ -70,6 +69,6 @@ describe("useResumeExport public PDF", () => {
await act(() => result.current.onDownloadPDF());
expect(mocks.downloadWithAnchor).not.toHaveBeenCalled();
expect(mocks.toastError).toHaveBeenCalledTimes(1);
expect(mocks.toastAdd).toHaveBeenCalledWith(expect.objectContaining({ type: "error" }));
});
});
@@ -3,11 +3,11 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { PublicResumePdfOptions } from "@/features/resume/public/public-pdf";
import { t } from "@lingui/core/macro";
import { useCallback, useState } from "react";
import { toast } from "sonner";
import { buildDocx } from "@reactive-resume/docx";
import { getResumeSectionTitle } from "@reactive-resume/pdf/section-title";
import { getResumeExportData, resumeHasCoverLetter } from "@reactive-resume/resume/export-sections";
import { buildMarkdown } from "@reactive-resume/resume/markdown";
import { toast } from "@reactive-resume/ui/components/toast";
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
import { resolvePublicResumePdfBlob } from "@/features/resume/public/public-pdf";
import { createSectionTitleResolverForLocale } from "@/libs/resume/section-title-locale";
@@ -80,7 +80,7 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
const blob = await buildDocx(data, resolveTitle);
downloadWithAnchor(blob, generateFilename(getTargetExportName(resume, target), "docx"));
} catch {
toast.error(t`There was a problem while generating the DOCX, please try again.`);
toast.add({ type: "error", description: t`There was a problem while generating the DOCX, please try again.` });
}
},
[resume],
@@ -90,7 +90,10 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
async (target: ResumeExportTarget = "resume", downloadOptions?: DownloadPdfOptions) => {
if (!resume) return;
if (target === "cover-letter" && !resumeHasCoverLetter(resume.data)) return;
const toastId = toast.loading(t`Please wait while your PDF is being generated...`);
const toastId = toast.add({
type: "loading",
description: t`Please wait while your PDF is being generated...`,
});
setIsExporting(true);
try {
const data = exportOptions.publicResumePdf ? resume.data : getResumeExportData(resume.data, target);
@@ -105,10 +108,10 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
);
downloadWithAnchor(blob, generateFilename(getTargetExportName(resume, target), "pdf"));
} catch {
toast.error(t`There was a problem while generating the PDF, please try again.`);
toast.add({ type: "error", description: t`There was a problem while generating the PDF, please try again.` });
} finally {
setIsExporting(false);
toast.dismiss(toastId);
toast.close(toastId);
}
},
[exportOptions.publicResumePdf, resume],
@@ -116,7 +119,7 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
const onPrint = useCallback(async () => {
if (!resume) return;
const toastId = toast.loading(t`Preparing your resume for printing...`);
const toastId = toast.add({ type: "loading", description: t`Preparing your resume for printing...` });
setIsExporting(true);
try {
const blob = exportOptions.publicResumePdf
@@ -142,10 +145,13 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
};
document.body.appendChild(iframe);
} catch {
toast.error(t`There was a problem while preparing your resume for printing, please try again.`);
toast.add({
type: "error",
description: t`There was a problem while preparing your resume for printing, please try again.`,
});
} finally {
setIsExporting(false);
toast.dismiss(toastId);
toast.close(toastId);
}
}, [exportOptions.publicResumePdf, resume]);
@@ -9,7 +9,7 @@ import { ResumePreviewClient } from "./preview.browser";
const previewMock = vi.hoisted(() => ({
builderResumeData: undefined as ResumeData | undefined,
toastError: vi.fn(),
toastAdd: vi.fn(),
toBlob: vi.fn(async () => new Blob(["%PDF"], { type: "application/pdf" })),
}));
@@ -40,8 +40,8 @@ vi.mock("@/features/resume/export/pdf-document", () => ({
createResumePdfBlob: previewMock.toBlob,
}));
vi.mock("sonner", () => ({
toast: { error: previewMock.toastError },
vi.mock("@reactive-resume/ui/components/toast", () => ({
toast: { add: previewMock.toastAdd },
}));
vi.mock("../builder/draft", () => ({
@@ -85,7 +85,7 @@ describe("ResumePreviewClient", () => {
previewMock.builderResumeData = undefined;
previewMock.toBlob.mockReset();
previewMock.toBlob.mockImplementation(async () => new Blob(["%PDF"], { type: "application/pdf" }));
previewMock.toastError.mockReset();
previewMock.toastAdd.mockReset();
});
it("renders a loading placeholder for each builder layout page while the PDF is generated", () => {
@@ -169,7 +169,7 @@ describe("ResumePreviewClient", () => {
view.rerender(<ResumePreviewClient pageLayout="vertical" pageScale={1.25} showPageNumbers={false} />);
await waitFor(() => expect(previewMock.toBlob).toHaveBeenCalledTimes(2));
await waitFor(() => expect(previewMock.toastError).toHaveBeenCalledTimes(1));
await waitFor(() => expect(previewMock.toastAdd).toHaveBeenCalledTimes(1));
expect(screen.getByRole("img", { name: "Resume page 1 of 1" })).toBeTruthy();
});
});
@@ -5,7 +5,7 @@ import type { PreviewPageSize } from "./preview.shared.utils";
import { t } from "@lingui/core/macro";
import { AnimatePresence, m } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { toast } from "@reactive-resume/ui/components/toast";
import { isRTL } from "@reactive-resume/utils/locale";
import { cn } from "@reactive-resume/utils/style";
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
@@ -136,7 +136,9 @@ export function ResumePreviewClient({
}
} catch {
if (cancelled || requestId !== requestIdRef.current) return;
toast.error(t`The resume preview could not be updated. The last valid preview is still shown.`, {
toast.add({
type: "error",
description: t`The resume preview could not be updated. The last valid preview is still shown.`,
id: "resume-preview-render-error",
});
}
@@ -11,8 +11,8 @@ import {
} from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { useCallback } from "react";
import { toast } from "sonner";
import { match } from "ts-pattern";
import { toast } from "@reactive-resume/ui/components/toast";
import { authClient } from "@/libs/auth/client";
import { getReadableErrorMessage } from "@/libs/error-message";
import { orpc } from "@/libs/orpc/client";
@@ -108,48 +108,53 @@ export function useAuthAccounts() {
export function useAuthProviderActions() {
const link = useCallback(async (provider: AuthProvider) => {
const providerName = getProviderName(provider);
const toastId = toast.loading(t`Linking your ${providerName} account...`);
const toastId = toast.add({ type: "loading", description: t`Linking your ${providerName} account...` });
const { error } = await authClient.linkSocial({ provider, callbackURL: "/dashboard/settings/authentication" });
if (error) {
toast.error(
getReadableErrorMessage(
toast.add({
type: "error",
description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when linking a social authentication provider fails",
message: "Failed to link provider. Please try again.",
}),
),
{ id: toastId },
);
id: toastId,
});
return;
}
toast.dismiss(toastId);
toast.close(toastId);
}, []);
const unlink = useCallback(async (provider: AuthProvider, accountId: string) => {
const providerName = getProviderName(provider);
const toastId = toast.loading(t`Unlinking your ${providerName} account...`);
const toastId = toast.add({
type: "loading",
description: t`Unlinking your ${providerName} account...`,
});
const { error } = await authClient.unlinkAccount({ providerId: provider, accountId });
if (error) {
toast.error(
getReadableErrorMessage(
toast.add({
type: "error",
description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when unlinking a social authentication provider fails",
message: "Failed to unlink provider. Please try again.",
}),
),
{ id: toastId },
);
id: toastId,
});
return;
}
toast.dismiss(toastId);
toast.close(toastId);
}, []);
return { link, unlink };
@@ -3,9 +3,9 @@ import { Trans } from "@lingui/react/macro";
import { KeyIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { m } from "motion/react";
import { toast } from "sonner";
import { Button } from "@reactive-resume/ui/components/button";
import { Separator } from "@reactive-resume/ui/components/separator";
import { toast } from "@reactive-resume/ui/components/toast";
import { usePrompt } from "@/hooks/use-prompt";
import { authClient } from "@/libs/auth/client";
import { getReadableErrorMessage } from "@/libs/error-message";
@@ -24,19 +24,20 @@ export function PasskeysSection() {
mutationFn: () => authClient.passkey.addPasskey(),
onSuccess: async ({ data, error }) => {
if (error) {
toast.error(
getReadableErrorMessage(
toast.add({
type: "error",
description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when passkey registration fails",
message: "Failed to register passkey. Please try again.",
}),
),
);
});
return;
}
toast.success(t`Passkey registered successfully.`);
toast.add({ type: "success", description: t`Passkey registered successfully.` });
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
const name = await prompt(t`Enter a name for your passkey.`, {
@@ -55,22 +56,23 @@ export function PasskeysSection() {
const { error: renameError } = await authClient.passkey.updatePasskey({ id: passkeyId, name: passkeyName });
if (renameError) {
toast.error(
getReadableErrorMessage(
toast.add({
type: "error",
description: getReadableErrorMessage(
renameError,
t({
comment: "Fallback toast when renaming a passkey fails",
message: "Failed to rename passkey. Please try again.",
}),
),
);
});
return;
}
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
},
onError: () => {
toast.error(t`Failed to register passkey. Please try again.`);
toast.add({ type: "error", description: t`Failed to register passkey. Please try again.` });
},
});
@@ -78,23 +80,24 @@ export function PasskeysSection() {
mutationFn: (id: string) => authClient.passkey.deletePasskey({ id }),
onSuccess: async ({ error }) => {
if (error) {
toast.error(
getReadableErrorMessage(
toast.add({
type: "error",
description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when deleting a passkey fails",
message: "Failed to delete passkey. Please try again.",
}),
),
);
});
return;
}
toast.success(t`Passkey deleted successfully.`);
toast.add({ type: "success", description: t`Passkey deleted successfully.` });
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
},
onError: () => {
toast.error(t`Failed to delete passkey. Please try again.`);
toast.add({ type: "error", description: t`Failed to delete passkey. Please try again.` });
},
});
@@ -4,7 +4,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { toast } from "sonner";
import { toast } from "@reactive-resume/ui/components/toast";
type MutationName = "create" | "test" | "update" | "delete";
@@ -100,7 +100,7 @@ vi.mock("@/components/ui/combobox", () => ({
),
}));
vi.mock("sonner", () => ({ toast: { error: vi.fn(), success: vi.fn() } }));
vi.mock("@reactive-resume/ui/components/toast", () => ({ toast: { add: vi.fn() } }));
i18n.loadAndActivate({ locale: "en", messages: {} });
@@ -208,11 +208,13 @@ describe("AISettingsSection", () => {
renderSection();
fireEvent.click(screen.getByRole("button", { name: "Test" }));
await waitFor(() => expect(toast.error).toHaveBeenCalled());
await waitFor(() => expect(toast.add).toHaveBeenCalledWith(expect.objectContaining({ type: "error" })));
// Toast reports only the outcome; the card carries the detail.
expect(toast.error).toHaveBeenCalledWith("Connection failed.");
expect(toast.error).not.toHaveBeenCalledWith(expect.stringContaining("rejected the API key"));
expect(toast.add).toHaveBeenCalledWith({ type: "error", description: "Connection failed." });
expect(toast.add).not.toHaveBeenCalledWith(
expect.objectContaining({ description: expect.stringContaining("rejected the API key") }),
);
providers.data = [failed];
renderSection();
@@ -15,7 +15,6 @@ import {
} from "@phosphor-icons/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { AI_PROVIDER_DEFAULT_BASE_URLS } from "@reactive-resume/ai/types";
import { Badge } from "@reactive-resume/ui/components/badge";
import { Button } from "@reactive-resume/ui/components/button";
@@ -23,6 +22,7 @@ import { Input } from "@reactive-resume/ui/components/input";
import { Label } from "@reactive-resume/ui/components/label";
import { Spinner } from "@reactive-resume/ui/components/spinner";
import { Switch } from "@reactive-resume/ui/components/switch";
import { toast } from "@reactive-resume/ui/components/toast";
import { cn } from "@reactive-resume/utils/style";
import { Combobox } from "@/components/ui/combobox";
import { useHasUsableAiProvider } from "@/features/settings/integrations/hooks/use-has-usable-ai-provider";
@@ -263,7 +263,11 @@ function ProviderRow({ provider }: ProviderRowProps) {
setIsEditingModel(false);
void invalidate();
},
onError: (error) => toast.error(getOrpcErrorMessage(error, { fallback: t`Failed to update provider.` })),
onError: (error) =>
toast.add({
type: "error",
description: getOrpcErrorMessage(error, { fallback: t`Failed to update provider.` }),
}),
},
);
};
@@ -325,7 +329,10 @@ function ProviderRow({ provider }: ProviderRowProps) {
{
onSuccess: () => void invalidate(),
onError: (error) =>
toast.error(getOrpcErrorMessage(error, { fallback: t`Failed to update provider.` })),
toast.add({
type: "error",
description: getOrpcErrorMessage(error, { fallback: t`Failed to update provider.` }),
}),
},
)
}
@@ -344,15 +351,18 @@ function ProviderRow({ provider }: ProviderRowProps) {
{
onSuccess: (response) => {
if (response.testStatus === "success") {
toast.success(t`Provider connection verified.`);
toast.add({ type: "success", description: t`Provider connection verified.` });
} else {
// The reason persists on the card below, so the toast only reports the outcome.
toast.error(t`Connection failed.`);
toast.add({ type: "error", description: t`Connection failed.` });
}
void invalidate();
},
onError: (error) => {
toast.error(getOrpcErrorMessage(error, { fallback: t`Could not verify provider connection.` }));
toast.add({
type: "error",
description: getOrpcErrorMessage(error, { fallback: t`Could not verify provider connection.` }),
});
void invalidate();
},
},
@@ -388,7 +398,10 @@ function ProviderRow({ provider }: ProviderRowProps) {
{
onSuccess: () => void invalidate(),
onError: (error) =>
toast.error(getOrpcErrorMessage(error, { fallback: t`Failed to delete provider.` })),
toast.add({
type: "error",
description: getOrpcErrorMessage(error, { fallback: t`Failed to delete provider.` }),
}),
},
)
}
@@ -3,9 +3,9 @@ import { Trans } from "@lingui/react/macro";
import { BookOpenIcon, KeyIcon, LinkSimpleIcon, PlusIcon, TrashSimpleIcon } from "@phosphor-icons/react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { AnimatePresence, m } from "motion/react";
import { toast } from "sonner";
import { Button } from "@reactive-resume/ui/components/button";
import { Separator } from "@reactive-resume/ui/components/separator";
import { toast } from "@reactive-resume/ui/components/toast";
import { useDialogStore } from "@/dialogs/store";
import { useConfirm } from "@/hooks/use-confirm";
import { authClient } from "@/libs/auth/client";
@@ -43,25 +43,26 @@ export function ApiKeysSettingsPage() {
if (!confirmation) return;
const toastId = toast.loading(t`Deleting your API key...`);
const toastId = toast.add({ type: "loading", description: t`Deleting your API key...` });
const { error } = await authClient.apiKey.delete({ keyId: id });
if (error) {
toast.error(
getReadableErrorMessage(
toast.add({
type: "error",
description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when deleting an API key fails",
message: "Failed to delete the API key. Please try again.",
}),
),
{ id: toastId },
);
id: toastId,
});
return;
}
toast.success(t`The API key has been deleted successfully.`, { id: toastId });
toast.add({ type: "success", description: t`The API key has been deleted successfully.`, id: toastId });
void queryClient.invalidateQueries({ queryKey: ["auth", "api-keys"] });
};
@@ -5,11 +5,11 @@ import { CheckIcon, WarningIcon } from "@phosphor-icons/react";
import { useStore } from "@tanstack/react-form";
import { useRouteContext, useRouter } from "@tanstack/react-router";
import { AnimatePresence, m } from "motion/react";
import { toast } from "sonner";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
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 { authClient } from "@/libs/auth/client";
import { getReadableErrorMessage } from "@/libs/error-message";
import { useAppForm } from "@/libs/tanstack-form";
@@ -51,19 +51,20 @@ export function ProfileSettingsPage({ session }: Props) {
});
if (error) {
toast.error(
getReadableErrorMessage(
toast.add({
type: "error",
description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when updating profile details fails",
message: "Failed to update your profile. Please try again.",
}),
),
);
});
return;
}
toast.success(t`Your profile has been updated successfully.`);
toast.add({ type: "success", description: t`Your profile has been updated successfully.` });
form.reset({ name: value.name, username: value.username, email: session.user.email });
void router.invalidate();
@@ -74,21 +75,23 @@ export function ProfileSettingsPage({ session }: Props) {
});
if (error) {
toast.error(
getReadableErrorMessage(
toast.add({
type: "error",
description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when requesting email change confirmation fails",
message: "Failed to request email change. Please try again.",
}),
),
);
});
return;
}
toast.success(
t`A confirmation link has been sent to your current email address. Please check your inbox to confirm the change.`,
);
toast.add({
type: "success",
description: t`A confirmation link has been sent to your current email address. Please check your inbox to confirm the change.`,
});
form.reset({ name: value.name, username: value.username, email: session.user.email });
void router.invalidate();
}
@@ -102,7 +105,7 @@ export function ProfileSettingsPage({ session }: Props) {
const isDirty = useStore(form.store, (s) => s.isDirty);
const handleResendVerificationEmail = async () => {
const toastId = toast.loading(t`Resending verification email...`);
const toastId = toast.add({ type: "loading", description: t`Resending verification email...` });
const { error } = await authClient.sendVerificationEmail({
email: session.user.email,
@@ -110,23 +113,25 @@ export function ProfileSettingsPage({ session }: Props) {
});
if (error) {
toast.error(
getReadableErrorMessage(
toast.add({
type: "error",
description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when resending account verification email fails",
message: "Failed to resend verification email. Please try again.",
}),
),
{ id: toastId },
);
id: toastId,
});
return;
}
toast.success(
t`A new verification link has been sent to your email address. Please check your inbox to verify your account.`,
{ id: toastId },
);
toast.add({
type: "success",
description: t`A new verification link has been sent to your email address. Please check your inbox to verify your account.`,
id: toastId,
});
void router.invalidate();
};
+8 -7
View File
@@ -4,7 +4,6 @@ import { useLingui } from "@lingui/react";
import { Trans } from "@lingui/react/macro";
import { PaletteIcon, SignOutIcon, TranslateIcon } from "@phosphor-icons/react";
import { useRouter } from "@tanstack/react-router";
import { toast } from "sonner";
import { useIsClient } from "usehooks-ts";
import {
DropdownMenu,
@@ -19,6 +18,7 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@reactive-resume/ui/components/dropdown-menu";
import { toast } from "@reactive-resume/ui/components/toast";
import { useTheme } from "@/features/theme/provider";
import { authClient } from "@/libs/auth/client";
import { getReadableErrorMessage } from "@/libs/error-message";
@@ -42,25 +42,26 @@ export function UserDropdownMenu({ children }: Props) {
};
const handleLogout = async () => {
const toastId = toast.loading(t`Signing out...`);
const toastId = toast.add({ type: "loading", description: t`Signing out...` });
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
toast.dismiss(toastId);
toast.close(toastId);
void router.invalidate();
},
onError: ({ error }) => {
toast.error(
getReadableErrorMessage(
toast.add({
type: "error",
description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when signing out fails",
message: "Failed to sign out. Please try again.",
}),
),
{ id: toastId },
);
id: toastId,
});
},
},
});
+2 -2
View File
@@ -17,7 +17,7 @@ import { createRootRouteWithContext, HeadContent, Outlet, useRouterState } from
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { domAnimation, LazyMotion, MotionConfig } from "motion/react";
import { useEffect } from "react";
import { Toaster } from "@reactive-resume/ui/components/sonner";
import { Toaster } from "@reactive-resume/ui/components/toast";
import { TooltipProvider } from "@reactive-resume/ui/components/tooltip";
import { BreakpointIndicator } from "@/components/layout/breakpoint-indicator";
import { DonationToast } from "@/components/ui/donation-toast";
@@ -136,7 +136,7 @@ function RootComponent() {
{!isBuilder && <DonationToast />}
<DialogManager />
<CommandPalette />
<Toaster richColors position="bottom-center" />
<Toaster />
{import.meta.env.DEV && <BreakpointIndicator />}
{import.meta.env.DEV && (
@@ -5,12 +5,12 @@ import { ArrowRightIcon, ChatCircleDotsIcon, FilePlusIcon, GearSixIcon } from "@
import { useMutation, useQuery } from "@tanstack/react-query";
import { Link, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { toast } from "sonner";
import { useIsClient } from "usehooks-ts";
import { Badge } from "@reactive-resume/ui/components/badge";
import { Button } from "@reactive-resume/ui/components/button";
import { Label } from "@reactive-resume/ui/components/label";
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 { getOrpcErrorMessage } from "@/libs/error-message";
@@ -176,15 +176,16 @@ export function NewThreadSetup({ resumeId }: NewThreadSetupProps) {
void navigate({ to: "/agent/$threadId", params: { threadId: thread.id } });
},
onError: (error) =>
toast.error(
getOrpcErrorMessage(error, {
toast.add({
type: "error",
description: getOrpcErrorMessage(error, {
byCode: {
PRECONDITION_FAILED: t`AI agent setup is unavailable until REDIS_URL and ENCRYPTION_SECRET are configured.`,
BAD_REQUEST: t`Set up an AI provider before starting a thread.`,
},
fallback: t`Failed to start agent thread.`,
}),
),
}),
},
)
}
@@ -5,8 +5,8 @@ import { Trans } from "@lingui/react/macro";
import { ArrowSquareOutIcon, CircleNotchIcon, FilePdfIcon, MinusIcon, PlusIcon } from "@phosphor-icons/react";
import { Link } from "@tanstack/react-router";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { Button } from "@reactive-resume/ui/components/button";
import { toast } from "@reactive-resume/ui/components/toast";
import { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/components/tooltip";
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
@@ -74,7 +74,10 @@ export function ResumePane({ resume }: ResumePaneProps) {
if (!resume) return;
const filename = generateFilename(resume.name || resume.data.basics.name || resume.id, "pdf");
const toastId = toast.loading(t`Please wait while your PDF is being generated…`);
const toastId = toast.add({
type: "loading",
description: t`Please wait while your PDF is being generated…`,
});
setIsPrinting(true);
@@ -82,10 +85,10 @@ export function ResumePane({ resume }: ResumePaneProps) {
const blob = await createResumePdfBlob(resume.data);
downloadWithAnchor(blob, filename);
} catch {
toast.error(t`There was a problem while generating the PDF, please try again.`);
toast.add({ type: "error", description: t`There was a problem while generating the PDF, please try again.` });
} finally {
setIsPrinting(false);
toast.dismiss(toastId);
toast.close(toastId);
}
}, [resume]);
@@ -13,7 +13,6 @@ import {
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, useNavigate } from "@tanstack/react-router";
import { useMemo } from "react";
import { toast } from "sonner";
import { Button } from "@reactive-resume/ui/components/button";
import {
DropdownMenu,
@@ -22,6 +21,7 @@ import {
DropdownMenuTrigger,
} from "@reactive-resume/ui/components/dropdown-menu";
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 { useConfirm } from "@/hooks/use-confirm";
import { getOrpcErrorMessage } from "@/libs/error-message";
@@ -62,7 +62,11 @@ function ThreadActions({ thread, activeThreadId }: ThreadActionsProps) {
});
}
},
onError: (error) => toast.error(getOrpcErrorMessage(error, { fallback: t`Failed to archive thread.` })),
onError: (error) =>
toast.add({
type: "error",
description: getOrpcErrorMessage(error, { fallback: t`Failed to archive thread.` }),
}),
},
);
};
@@ -81,7 +85,11 @@ function ThreadActions({ thread, activeThreadId }: ThreadActionsProps) {
await queryClient.invalidateQueries({ queryKey: orpc.agent.threads.list.queryKey() });
if (activeThreadId === thread.id) void navigate({ to: "/agent" });
},
onError: (error) => toast.error(getOrpcErrorMessage(error, { fallback: t`Failed to delete thread.` })),
onError: (error) =>
toast.add({
type: "error",
description: getOrpcErrorMessage(error, { fallback: t`Failed to delete thread.` }),
}),
},
);
};
@@ -16,7 +16,6 @@ import { useHotkey } from "@tanstack/react-hotkeys";
import { useNavigate } from "@tanstack/react-router";
import { m } from "motion/react";
import { useControls, useTransformComponent } from "react-zoom-pan-pinch";
import { toast } from "sonner";
import { useCopyToClipboard } from "usehooks-ts";
import { Button } from "@reactive-resume/ui/components/button";
import {
@@ -25,6 +24,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@reactive-resume/ui/components/dropdown-menu";
import { toast } from "@reactive-resume/ui/components/toast";
import { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/components/tooltip";
import { cn } from "@reactive-resume/utils/style";
import {
@@ -108,7 +108,7 @@ export function BuilderDock({ pageLayout, onTogglePageLayout }: BuilderDockProps
title={t`Copy URL`}
onClick={async () => {
await copyToClipboard(publicUrl);
toast.success(t`A link to your resume has been copied to clipboard.`);
toast.add({ type: "success", description: t`A link to your resume has been copied to clipboard.` });
}}
/>
</m.div>
@@ -16,7 +16,6 @@ import {
} from "@phosphor-icons/react";
import { useMutation } from "@tanstack/react-query";
import { Link, useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
import { match } from "ts-pattern";
import { Button } from "@reactive-resume/ui/components/button";
import {
@@ -26,6 +25,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@reactive-resume/ui/components/dropdown-menu";
import { toast } from "@reactive-resume/ui/components/toast";
import { useDialogStore } from "@/dialogs/store";
import {
useCurrentBuilderResumeSelector,
@@ -205,7 +205,7 @@ function BuilderHeaderDropdown() {
});
},
onError: (error) => {
toast.error(getResumeErrorMessage(error));
toast.add({ type: "error", description: getResumeErrorMessage(error) });
},
},
);
@@ -218,17 +218,17 @@ function BuilderHeaderDropdown() {
if (!confirmation) return;
const toastId = toast.loading(t`Deleting your resume...`);
const toastId = toast.add({ type: "loading", description: t`Deleting your resume...` });
deleteResume(
{ id },
{
onSuccess: () => {
toast.success(t`Your resume has been deleted successfully.`, { id: toastId });
toast.add({ type: "success", description: t`Your resume has been deleted successfully.`, id: toastId });
void navigate({ to: "/dashboard/resumes", search: { sort: "lastUpdatedAt", tags: [] } });
},
onError: (error) => {
toast.error(getResumeErrorMessage(error), { id: toastId });
toast.add({ type: "error", description: getResumeErrorMessage(error), id: toastId });
},
},
);
@@ -1,9 +1,8 @@
import { t } from "@lingui/core/macro";
import { FloppyDiskIcon } from "@phosphor-icons/react";
import { useHotkey } from "@tanstack/react-hotkeys";
import { Suspense, useState } from "react";
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
import { toast } from "sonner";
import { toast } from "@reactive-resume/ui/components/toast";
import { LoadingScreen } from "@/components/layout/loading-screen";
import { ResumePreview } from "@/features/resume/preview/preview";
import { BuilderDock } from "./dock";
@@ -14,7 +13,11 @@ export function PreviewPage() {
const [pageLayout, setPageLayout] = useState(DEFAULT_BUILDER_PREVIEW_PAGE_LAYOUT);
useHotkey("Mod+S", () => {
toast.info(t`Your changes are saved automatically.`, { id: "auto-save", icon: <FloppyDiskIcon /> });
toast.add({
type: "info",
description: t`Your changes are saved automatically.`,
id: "auto-save",
});
});
return (
@@ -5,7 +5,6 @@ import { Trans } from "@lingui/react/macro";
import { ClockCounterClockwiseIcon } from "@phosphor-icons/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { Button } from "@reactive-resume/ui/components/button";
import {
DropdownMenu,
@@ -16,6 +15,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@reactive-resume/ui/components/dropdown-menu";
import { toast } from "@reactive-resume/ui/components/toast";
import { useResumeStore } from "@/features/resume/builder/draft";
import { useConfirm } from "@/hooks/use-confirm";
import { getResumeErrorMessage } from "@/libs/error-message";
@@ -53,9 +53,9 @@ export function BuilderVersionHistory({ resumeId }: BuilderVersionHistoryProps)
replaceResumeFromServer(restored as Resume);
queryClient.setQueryData(orpc.resume.getById.queryOptions({ input: { id: resumeId } }).queryKey, restored);
void queryClient.invalidateQueries({ queryKey: orpc.resume.listVersions.queryKey({ input: { resumeId } }) });
toast.success(t`Your resume has been restored to the selected version.`);
toast.add({ type: "success", description: t`Your resume has been restored to the selected version.` });
} catch (error) {
toast.error(getResumeErrorMessage(error));
toast.add({ type: "error", description: getResumeErrorMessage(error) });
}
};
@@ -4,12 +4,12 @@ import { Trans } from "@lingui/react/macro";
import { LockSimpleIcon } from "@phosphor-icons/react";
import { useMutation } from "@tanstack/react-query";
import { Fragment, useCallback, useRef } from "react";
import { toast } from "sonner";
import { match } from "ts-pattern";
import { Avatar, AvatarFallback, AvatarImage } from "@reactive-resume/ui/components/avatar";
import { Button } from "@reactive-resume/ui/components/button";
import { ScrollArea } from "@reactive-resume/ui/components/scroll-area";
import { Separator } from "@reactive-resume/ui/components/separator";
import { toast } from "@reactive-resume/ui/components/toast";
import { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/components/tooltip";
import { getInitials } from "@reactive-resume/utils/string";
import { useCurrentResume, useIsResumeLocked, usePatchResume } from "@/features/resume/builder/draft";
@@ -98,7 +98,7 @@ function LockBanner() {
});
},
onError: (error) => {
toast.error(getResumeErrorMessage(error));
toast.add({ type: "error", description: getResumeErrorMessage(error) });
},
},
);
@@ -13,7 +13,6 @@ import {
import { useMutation } from "@tanstack/react-query";
import { useRef, useState } from "react";
import Cropper from "react-easy-crop";
import { toast } from "sonner";
import { pictureSchema } from "@reactive-resume/schema/resume/data";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { Button } from "@reactive-resume/ui/components/button";
@@ -35,6 +34,7 @@ import {
InputGroupText,
} from "@reactive-resume/ui/components/input-group";
import { Slider } from "@reactive-resume/ui/components/slider";
import { toast } from "@reactive-resume/ui/components/toast";
import "react-easy-crop/react-easy-crop.css";
import { ColorPicker } from "@/components/input/color-picker";
import { useCurrentBuilderResumeSelector, useUpdateResumeData } from "@/features/resume/builder/draft";
@@ -490,26 +490,27 @@ function PictureSectionForm() {
};
const uploadPictureFile = (file: File) => {
const toastId = toast.loading(t`Uploading picture…`);
const toastId = toast.add({ type: "loading", description: t`Uploading picture…` });
uploadFile(file, {
onSuccess: ({ url }) => {
form.setFieldValue("url", url);
handleAutoSave();
toast.dismiss(toastId);
toast.close(toastId);
if (fileInputRef.current) fileInputRef.current.value = "";
},
onError: (error) => {
toast.error(
getReadableErrorMessage(
toast.add({
type: "error",
description: getReadableErrorMessage(
error,
t({
comment: "Fallback toast when uploading profile picture for resume fails",
message: "Failed to upload picture. Please try again.",
}),
),
{ id: toastId },
);
id: toastId,
});
},
});
};
@@ -3,13 +3,12 @@ import { Trans } from "@lingui/react/macro";
import { ArrowRightIcon, InfoIcon, LightningIcon, SparkleIcon } from "@phosphor-icons/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { toast } from "sonner";
import { match } from "ts-pattern";
import { Alert, AlertDescription } from "@reactive-resume/ui/components/alert";
import { Badge } from "@reactive-resume/ui/components/badge";
import { Button } from "@reactive-resume/ui/components/button";
import { toast } from "@reactive-resume/ui/components/toast";
import { useResume } from "@/features/resume/builder/draft";
import { getOrpcErrorMessage } from "@/libs/error-message";
import { orpc } from "@/libs/orpc/client";
import { SectionBase } from "../shared/section-base";
@@ -62,27 +61,10 @@ export function ResumeAnalysisSectionBuilder() {
...orpc.ai.analyzeResume.mutationOptions(),
onSuccess: (analysis) => {
queryClient.setQueryData(orpc.resume.analysis.getById.queryKey({ input: { id: resumeId } }), analysis);
toast.success(t`Resume analysis complete.`);
toast.add({ type: "success", description: t`Resume analysis complete.` });
},
onError: (error) => {
toast.error(t`Failed to analyze resume.`, {
description: getOrpcErrorMessage(error, {
byCode: {
BAD_REQUEST: t({
comment: "Error description when AI returns invalid resume analysis format",
message: "The AI returned an invalid analysis format. Please try again.",
}),
BAD_GATEWAY: t({
comment: "Error description when AI provider cannot be reached during resume analysis",
message: "Could not reach the AI provider. Please try again.",
}),
},
fallback: t({
comment: "Fallback error description when resume analysis request fails",
message: "Something went wrong while analyzing your resume.",
}),
}),
});
onError: (_error) => {
toast.add({ type: "error", description: t`Failed to analyze resume.` });
},
});
@@ -4,12 +4,12 @@ import { ORPCError } from "@orpc/client";
import { ClipboardIcon, LockSimpleIcon, LockSimpleOpenIcon } from "@phosphor-icons/react";
import { useMutation } from "@tanstack/react-query";
import { useCallback } from "react";
import { toast } from "sonner";
import { useCopyToClipboard } from "usehooks-ts";
import { Button } from "@reactive-resume/ui/components/button";
import { Input } from "@reactive-resume/ui/components/input";
import { Label } from "@reactive-resume/ui/components/label";
import { Switch } from "@reactive-resume/ui/components/switch";
import { toast } from "@reactive-resume/ui/components/toast";
import { useCurrentResume, usePatchResume } from "@/features/resume/builder/draft";
import { useConfirm } from "@/hooks/use-confirm";
import { usePrompt } from "@/hooks/use-prompt";
@@ -33,7 +33,7 @@ export function SharingSectionBuilder() {
const onCopyUrl = useCallback(async () => {
await copyToClipboard(publicUrl);
toast.success(t`A link to your resume has been copied to clipboard.`);
toast.add({ type: "success", description: t`A link to your resume has been copied to clipboard.` });
}, [publicUrl, copyToClipboard]);
const onTogglePublic = useCallback(
@@ -45,7 +45,7 @@ export function SharingSectionBuilder() {
});
} catch (error) {
const message = error instanceof ORPCError ? error.message : t`Something went wrong. Please try again.`;
toast.error(message);
toast.add({ type: "error", description: message });
}
},
[patchResume, resume.id, updateResume],
@@ -64,19 +64,19 @@ export function SharingSectionBuilder() {
if (!value) return;
const password = value.trim();
if (!password) return toast.error(t`Password cannot be empty.`);
if (!password) return toast.add({ type: "error", description: t`Password cannot be empty.` });
const toastId = toast.loading(t`Enabling password protection...`);
const toastId = toast.add({ type: "loading", description: t`Enabling password protection...` });
try {
await setPassword({ id: resume.id, password });
patchResume((draft) => {
draft.hasPassword = true;
});
toast.success(t`Password protection has been enabled.`, { id: toastId });
toast.add({ type: "success", description: t`Password protection has been enabled.`, id: toastId });
} catch (error) {
const message = error instanceof ORPCError ? error.message : t`Something went wrong. Please try again.`;
toast.error(message, { id: toastId });
toast.add({ type: "error", description: message, id: toastId });
}
}, [patchResume, prompt, resume.id, setPassword]);
@@ -90,17 +90,17 @@ export function SharingSectionBuilder() {
});
if (!confirmation) return;
const toastId = toast.loading(t`Removing password protection...`);
const toastId = toast.add({ type: "loading", description: t`Removing password protection...` });
try {
await removePassword({ id: resume.id });
patchResume((draft) => {
draft.hasPassword = false;
});
toast.success(t`Password protection has been disabled.`, { id: toastId });
toast.add({ type: "success", description: t`Password protection has been disabled.`, id: toastId });
} catch (error) {
const message = error instanceof ORPCError ? error.message : t`Something went wrong. Please try again.`;
toast.error(message, { id: toastId });
toast.add({ type: "error", description: message, id: toastId });
}
}, [confirm, patchResume, removePassword, resume.hasPassword, resume.id]);
@@ -1,7 +1,7 @@
import type { RouterOutput } from "@/libs/orpc/client";
import { t } from "@lingui/core/macro";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { toast } from "@reactive-resume/ui/components/toast";
import { useDialogStore } from "@/dialogs/store";
import { useConfirm } from "@/hooks/use-confirm";
import { getResumeErrorMessage } from "@/libs/error-message";
@@ -25,7 +25,7 @@ export function useResumeMenuActions(resume: Resume) {
setLockedResume(
{ id: resume.id, isLocked: !resume.isLocked },
{ onError: (error) => toast.error(getResumeErrorMessage(error)) },
{ onError: (error) => toast.add({ type: "error", description: getResumeErrorMessage(error) }) },
);
};
@@ -35,12 +35,13 @@ export function useResumeMenuActions(resume: Resume) {
});
if (!confirmed) return;
const toastId = toast.loading(t`Deleting your resume...`);
const toastId = toast.add({ type: "loading", description: t`Deleting your resume...` });
deleteResume(
{ id: resume.id },
{
onSuccess: () => toast.success(t`Your resume has been deleted successfully.`, { id: toastId }),
onError: (error) => toast.error(getResumeErrorMessage(error), { id: toastId }),
onSuccess: () =>
toast.add({ type: "success", description: t`Your resume has been deleted successfully.`, id: toastId }),
onError: (error) => toast.add({ type: "error", description: getResumeErrorMessage(error), id: toastId }),
},
);
};