mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 06:42:15 +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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user