From 23ceee214824ee513224ebf18216c4db306a2de0 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Mon, 17 Aug 2026 22:32:32 +0200 Subject: [PATCH] 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. --- apps/web/src/components/input/rich-input.tsx | 6 +- .../src/components/ui/donation-toast.test.tsx | 76 +++++++++---------- apps/web/src/components/ui/donation-toast.tsx | 71 +++++------------ apps/web/src/dialogs/api-key/create.tsx | 17 +++-- apps/web/src/dialogs/auth/change-password.tsx | 15 ++-- .../src/dialogs/auth/disable-two-factor.tsx | 22 ++++-- .../src/dialogs/auth/enable-two-factor.tsx | 36 ++++----- apps/web/src/dialogs/resume/import.tsx | 23 ++++-- apps/web/src/dialogs/resume/index.tsx | 26 +++---- .../src/dialogs/resume/template/gallery.tsx | 9 ++- .../components/application-actions-menu.tsx | 8 +- .../components/application-ai-copilot.tsx | 14 ++-- .../components/application-detail-sheet.tsx | 14 ++-- .../applications/components/board.tsx | 4 +- .../components/file-attachment-field.tsx | 11 +-- .../components/import-applications-sheet.tsx | 6 +- .../applications/components/insights-view.tsx | 4 +- .../applications/components/table-view.tsx | 8 +- .../features/auth/components/social-auth.tsx | 16 ++-- .../features/auth/pages/forgot-password.tsx | 16 ++-- apps/web/src/features/auth/pages/login.tsx | 20 ++--- apps/web/src/features/auth/pages/register.tsx | 16 ++-- .../features/auth/pages/reset-password.tsx | 18 +++-- .../features/auth/pages/resume-password.tsx | 17 +++-- .../src/features/auth/pages/verify-2fa.tsx | 19 +++-- .../src/features/resume/builder/draft.test.ts | 15 ++-- apps/web/src/features/resume/builder/draft.ts | 26 ++++--- .../resume/export/use-resume-export.test.tsx | 13 ++-- .../resume/export/use-resume-export.ts | 22 ++++-- .../resume/preview/preview.browser.test.tsx | 10 +-- .../resume/preview/preview.browser.tsx | 6 +- .../authentication/components/hooks.tsx | 31 ++++---- .../authentication/components/passkeys.tsx | 31 ++++---- .../components/ai-section.test.tsx | 12 +-- .../integrations/components/ai-section.tsx | 27 +++++-- .../src/features/settings/pages/api-keys.tsx | 15 ++-- .../src/features/settings/pages/profile.tsx | 45 ++++++----- apps/web/src/features/user/dropdown-menu.tsx | 15 ++-- apps/web/src/routes/__root.tsx | 4 +- .../agent/-components/new-thread-setup.tsx | 9 ++- .../routes/agent/-components/resume-pane.tsx | 11 ++- .../agent/-components/thread-sidebar.tsx | 14 +++- .../builder/$resumeId/-components/dock.tsx | 4 +- .../builder/$resumeId/-components/header.tsx | 10 +-- .../$resumeId/-components/preview-page.tsx | 9 ++- .../$resumeId/-components/version-history.tsx | 6 +- .../builder/$resumeId/-sidebar/left/index.tsx | 4 +- .../-sidebar/left/sections/picture.tsx | 15 ++-- .../right/sections/resume-analysis.tsx | 26 +------ .../-sidebar/right/sections/sharing.tsx | 20 ++--- .../menus/use-resume-menu-actions.ts | 11 +-- 51 files changed, 472 insertions(+), 431 deletions(-) diff --git a/apps/web/src/components/input/rich-input.tsx b/apps/web/src/components/input/rich-input.tsx index 1a64cd21c..34e1a5b06 100644 --- a/apps/web/src/components/input/rich-input.tsx +++ b/apps/web/src/components/input/rich-input.tsx @@ -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; diff --git a/apps/web/src/components/ui/donation-toast.test.tsx b/apps/web/src/components/ui/donation-toast.test.tsx index 4fa58ff3f..30ed6e9ce 100644 --- a/apps/web/src/components/ui/donation-toast.test.tsx +++ b/apps/web/src/components/ui/donation-toast.test.tsx @@ -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({customToast[0]("donation-toast")}); -}; - 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(); - 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(); 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(); 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"); }); }); diff --git a/apps/web/src/components/ui/donation-toast.tsx b/apps/web/src/components/ui/donation-toast.tsx index 81e9be77d..2617fed11 100644 --- a/apps/web/src/components/ui/donation-toast.tsx +++ b/apps/web/src/components/ui/donation-toast.tsx @@ -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) => 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 ( -
-
-
-
- -
-

- Please support the project -

-

- Reactive Resume is free and open source. If it has helped you, please consider donating. -

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

- Link a resume and add a job description (Edit) to score your fit and tailor a copy. + Link a resume and paste the job description (Edit) to score your fit and tailor a copy.

) : score == null ? (