mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-20 05:21:44 +10:00
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:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 }),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user