mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-24 07:12:18 +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 { EditorContent, EditorContext, useEditor, useEditorState } from "@tiptap/react";
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { match } from "ts-pattern";
|
import { match } from "ts-pattern";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
@@ -51,6 +50,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@reactive-resume/ui/components/dropdown-menu";
|
} from "@reactive-resume/ui/components/dropdown-menu";
|
||||||
import { PopoverHeader, PopoverTitle, PopoverTrigger } from "@reactive-resume/ui/components/popover";
|
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 { Toggle } from "@reactive-resume/ui/components/toggle";
|
||||||
import { isDarkColor } from "@reactive-resume/utils/color";
|
import { isDarkColor } from "@reactive-resume/utils/color";
|
||||||
import { cn } from "@reactive-resume/utils/style";
|
import { cn } from "@reactive-resume/utils/style";
|
||||||
@@ -319,7 +319,9 @@ function useEditorToolbarState(editor: Editor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!z.url({ protocol: /^https?$/ }).safeParse(url).success) {
|
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://.`,
|
description: t`Valid URLs must start with http:// or https://.`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
// @vitest-environment happy-dom
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
import type React from "react";
|
import { act, render } from "@testing-library/react";
|
||||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { i18n } from "@lingui/core";
|
import { i18n } from "@lingui/core";
|
||||||
import { I18nProvider } from "@lingui/react";
|
|
||||||
import { DonationToast } from "./donation-toast";
|
import { DonationToast } from "./donation-toast";
|
||||||
|
|
||||||
type ToastOptions = {
|
type AddOptions = {
|
||||||
dismissible: boolean;
|
actionProps: { children: string; onClick: () => void };
|
||||||
duration: number;
|
description: string;
|
||||||
id: string;
|
id: string;
|
||||||
unstyled: boolean;
|
onClose: () => void;
|
||||||
|
timeout: number;
|
||||||
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cookieMock = vi.hoisted(() => ({
|
const cookieMock = vi.hoisted(() => ({
|
||||||
@@ -21,8 +21,8 @@ const cookieMock = vi.hoisted(() => ({
|
|||||||
|
|
||||||
const toastMock = vi.hoisted(() => ({
|
const toastMock = vi.hoisted(() => ({
|
||||||
toast: {
|
toast: {
|
||||||
custom: vi.fn(),
|
add: vi.fn(),
|
||||||
dismiss: 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,
|
toast: toastMock.toast,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const getCustomToast = () =>
|
const getAddOptions = () => {
|
||||||
toastMock.toast.custom.mock.calls[0] as [(toastId: string | number) => React.ReactElement, ToastOptions] | undefined;
|
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 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", () => {
|
describe("DonationToast", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
@@ -56,8 +52,8 @@ describe("DonationToast", () => {
|
|||||||
i18n.loadAndActivate({ locale: "en-US", messages: {} });
|
i18n.loadAndActivate({ locale: "en-US", messages: {} });
|
||||||
cookieMock.value = null;
|
cookieMock.value = null;
|
||||||
cookieMock.set.mockClear();
|
cookieMock.set.mockClear();
|
||||||
toastMock.toast.custom.mockClear();
|
toastMock.toast.add.mockClear();
|
||||||
toastMock.toast.dismiss.mockClear();
|
toastMock.toast.close.mockClear();
|
||||||
vi.spyOn(window, "open").mockReturnValue(null);
|
vi.spyOn(window, "open").mockReturnValue(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,24 +65,22 @@ describe("DonationToast", () => {
|
|||||||
it("waits before showing the donation toast", () => {
|
it("waits before showing the donation toast", () => {
|
||||||
render(<DonationToast />);
|
render(<DonationToast />);
|
||||||
|
|
||||||
expect(toastMock.toast.custom).not.toHaveBeenCalled();
|
expect(toastMock.toast.add).not.toHaveBeenCalled();
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
vi.advanceTimersByTime(SHOW_TOAST_DELAY_MS - 1);
|
vi.advanceTimersByTime(SHOW_TOAST_DELAY_MS - 1);
|
||||||
});
|
});
|
||||||
expect(toastMock.toast.custom).not.toHaveBeenCalled();
|
expect(toastMock.toast.add).not.toHaveBeenCalled();
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
vi.advanceTimersByTime(1);
|
vi.advanceTimersByTime(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(toastMock.toast.custom).toHaveBeenCalledWith(
|
expect(toastMock.toast.add).toHaveBeenCalledWith(
|
||||||
expect.any(Function),
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
dismissible: false,
|
|
||||||
duration: Number.POSITIVE_INFINITY,
|
|
||||||
id: "donation-toast",
|
id: "donation-toast",
|
||||||
unstyled: true,
|
timeout: 0,
|
||||||
|
title: "Please support the project",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -100,18 +94,19 @@ describe("DonationToast", () => {
|
|||||||
vi.advanceTimersByTime(SHOW_TOAST_DELAY_MS);
|
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 />);
|
render(<DonationToast />);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
vi.advanceTimersByTime(SHOW_TOAST_DELAY_MS);
|
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", {
|
expect(cookieMock.set).toHaveBeenCalledWith("donation-toast-dismissed", "true", {
|
||||||
path: "/",
|
path: "/",
|
||||||
@@ -119,30 +114,27 @@ describe("DonationToast", () => {
|
|||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
expires: new Date("2026-06-10T12:05:00.000Z"),
|
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 />);
|
render(<DonationToast />);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
vi.advanceTimersByTime(SHOW_TOAST_DELAY_MS);
|
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", {
|
act(() => {
|
||||||
path: "/",
|
options.actionProps.onClick();
|
||||||
secure: true,
|
|
||||||
sameSite: "lax",
|
|
||||||
expires: new Date("2026-06-10T12:05:00.000Z"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(window.open).toHaveBeenCalledWith(
|
expect(window.open).toHaveBeenCalledWith(
|
||||||
"https://opencollective.com/reactive-resume/donate",
|
"https://opencollective.com/reactive-resume/donate",
|
||||||
"_blank",
|
"_blank",
|
||||||
"noopener,noreferrer",
|
"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 { t } from "@lingui/core/macro";
|
||||||
import { HandHeartIcon } from "@phosphor-icons/react";
|
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useTimeout } from "usehooks-ts";
|
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 TOAST_ID = "donation-toast";
|
||||||
const SHOW_TOAST_DELAY_MS = 5 * 60 * 1000; // 5 minutes
|
const SHOW_TOAST_DELAY_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
@@ -26,22 +24,22 @@ export function DonationToast() {
|
|||||||
const showToast = useCallback(() => {
|
const showToast = useCallback(() => {
|
||||||
if (dismissed === "true") return;
|
if (dismissed === "true") return;
|
||||||
|
|
||||||
const onDonate = (t: string | number) => {
|
toast.add({
|
||||||
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)} />, {
|
|
||||||
id: TOAST_ID,
|
id: TOAST_ID,
|
||||||
unstyled: true,
|
// Never auto-dismisses: closing it is what records the 30-day cookie.
|
||||||
dismissible: false,
|
timeout: 0,
|
||||||
duration: Number.POSITIVE_INFINITY,
|
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]);
|
}, [dismissed, setDismissed]);
|
||||||
|
|
||||||
@@ -49,38 +47,3 @@ export function DonationToast() {
|
|||||||
|
|
||||||
return null;
|
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 { useStore } from "@tanstack/react-form";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useCopyToClipboard } from "usehooks-ts";
|
import { useCopyToClipboard } from "usehooks-ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
@@ -24,6 +23,7 @@ import {
|
|||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupInput,
|
InputGroupInput,
|
||||||
} from "@reactive-resume/ui/components/input-group";
|
} from "@reactive-resume/ui/components/input-group";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { Combobox } from "@/components/ui/combobox";
|
import { Combobox } from "@/components/ui/combobox";
|
||||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
@@ -56,7 +56,7 @@ const CreateApiKeyForm = ({ setApiKey }: CreateApiKeyFormProps) => {
|
|||||||
},
|
},
|
||||||
validators: { onSubmit: formSchema },
|
validators: { onSubmit: formSchema },
|
||||||
onSubmit: async ({ value }) => {
|
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({
|
const { data, error } = await authClient.apiKey.create({
|
||||||
name: value.name,
|
name: value.name,
|
||||||
@@ -64,21 +64,22 @@ const CreateApiKeyForm = ({ setApiKey }: CreateApiKeyFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when creating an API key fails",
|
comment: "Fallback toast when creating an API key fails",
|
||||||
message: "Failed to create API key. Please try again.",
|
message: "Failed to create API key. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setApiKey(data.key);
|
setApiKey(data.key);
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,7 +200,7 @@ const CopyApiKeyForm = ({ apiKey }: CopyApiKeyFormProps) => {
|
|||||||
|
|
||||||
const onCopy = async () => {
|
const onCopy = async () => {
|
||||||
await copyToClipboard(apiKey);
|
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 = () => {
|
const onConfirm = () => {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { t } from "@lingui/core/macro";
|
|||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { EyeIcon, EyeSlashIcon, PasswordIcon } from "@phosphor-icons/react";
|
import { EyeIcon, EyeSlashIcon, PasswordIcon } from "@phosphor-icons/react";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useToggle } from "usehooks-ts";
|
import { useToggle } from "usehooks-ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
} from "@reactive-resume/ui/components/dialog";
|
} from "@reactive-resume/ui/components/dialog";
|
||||||
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { getReadableErrorMessage } from "@/libs/error-message";
|
import { getReadableErrorMessage } from "@/libs/error-message";
|
||||||
@@ -48,7 +48,7 @@ export function ChangePasswordDialog(_: DialogProps<"auth.change-password">) {
|
|||||||
onSubmit: formSchema,
|
onSubmit: formSchema,
|
||||||
},
|
},
|
||||||
onSubmit: async ({ value }) => {
|
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({
|
const { error } = await authClient.changePassword({
|
||||||
currentPassword: value.currentPassword,
|
currentPassword: value.currentPassword,
|
||||||
@@ -56,20 +56,21 @@ export function ChangePasswordDialog(_: DialogProps<"auth.change-password">) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when changing account password fails",
|
comment: "Fallback toast when changing account password fails",
|
||||||
message: "Failed to update your password. Please try again.",
|
message: "Failed to update your password. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
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"] });
|
void queryClient.invalidateQueries({ queryKey: ["auth", "accounts"] });
|
||||||
closeDialog();
|
closeDialog();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { t } from "@lingui/core/macro";
|
|||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { EyeIcon, EyeSlashIcon, LockOpenIcon } from "@phosphor-icons/react";
|
import { EyeIcon, EyeSlashIcon, LockOpenIcon } from "@phosphor-icons/react";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useToggle } from "usehooks-ts";
|
import { useToggle } from "usehooks-ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
} from "@reactive-resume/ui/components/dialog";
|
} from "@reactive-resume/ui/components/dialog";
|
||||||
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { getReadableErrorMessage } from "@/libs/error-message";
|
import { getReadableErrorMessage } from "@/libs/error-message";
|
||||||
@@ -35,25 +35,33 @@ export function DisableTwoFactorDialog(_: DialogProps<"auth.two-factor.disable">
|
|||||||
defaultValues: { password: "" },
|
defaultValues: { password: "" },
|
||||||
validators: { onSubmit: formSchema },
|
validators: { onSubmit: formSchema },
|
||||||
onSubmit: async ({ value }) => {
|
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 });
|
const { error } = await authClient.twoFactor.disable({ password: value.password });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when disabling two-factor authentication fails",
|
comment: "Fallback toast when disabling two-factor authentication fails",
|
||||||
message: "Failed to disable two-factor authentication. Please try again.",
|
message: "Failed to disable two-factor authentication. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
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();
|
void router.invalidate();
|
||||||
closeDialog();
|
closeDialog();
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useStore } from "@tanstack/react-form";
|
|||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
import { QRCodeSVG } from "qrcode.react";
|
import { QRCodeSVG } from "qrcode.react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { match } from "ts-pattern";
|
import { match } from "ts-pattern";
|
||||||
import { useToggle } from "usehooks-ts";
|
import { useToggle } from "usehooks-ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
} from "@reactive-resume/ui/components/dialog";
|
} from "@reactive-resume/ui/components/dialog";
|
||||||
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
import { useFormBlocker } from "@/hooks/use-form-blocker";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { getReadableErrorMessage } from "@/libs/error-message";
|
import { getReadableErrorMessage } from "@/libs/error-message";
|
||||||
@@ -58,7 +58,7 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
|
|||||||
defaultValues: { password: "" },
|
defaultValues: { password: "" },
|
||||||
validators: { onSubmit: enableFormSchema },
|
validators: { onSubmit: enableFormSchema },
|
||||||
onSubmit: async ({ value }) => {
|
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({
|
const { data, error } = await authClient.twoFactor.enable({
|
||||||
password: value.password,
|
password: value.password,
|
||||||
@@ -66,16 +66,17 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when enabling two-factor authentication fails",
|
comment: "Fallback toast when enabling two-factor authentication fails",
|
||||||
message: "Failed to enable two-factor authentication. Please try again.",
|
message: "Failed to enable two-factor authentication. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,9 +84,9 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
|
|||||||
setTotpUri(data.totpURI);
|
setTotpUri(data.totpURI);
|
||||||
setBackupCodes(data.backupCodes);
|
setBackupCodes(data.backupCodes);
|
||||||
setStep("verify");
|
setStep("verify");
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
} else {
|
} 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: "" },
|
defaultValues: { code: "" },
|
||||||
validators: { onSubmit: verifyFormSchema },
|
validators: { onSubmit: verifyFormSchema },
|
||||||
onSubmit: async ({ value }) => {
|
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 });
|
const { error } = await authClient.twoFactor.verifyTotp({ code: value.code });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when verifying two-factor setup code fails",
|
comment: "Fallback toast when verifying two-factor setup code fails",
|
||||||
message: "Failed to verify your code. Please try again.",
|
message: "Failed to verify your code. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
setStep("backup");
|
setStep("backup");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -131,7 +133,7 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onConfirmBackup = () => {
|
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();
|
void router.invalidate();
|
||||||
closeDialog();
|
closeDialog();
|
||||||
onReset();
|
onReset();
|
||||||
@@ -150,13 +152,13 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
|
|||||||
const secret = extractSecretFromTotpUri(totpUri);
|
const secret = extractSecretFromTotpUri(totpUri);
|
||||||
if (!secret) return;
|
if (!secret) return;
|
||||||
await navigator.clipboard.writeText(secret);
|
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 () => {
|
const handleCopyBackupCodes = async () => {
|
||||||
if (!backupCodes) return;
|
if (!backupCodes) return;
|
||||||
await navigator.clipboard.writeText(backupCodes.join("\n"));
|
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 = () => {
|
const handleDownloadBackupCodes = () => {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useStore } from "@tanstack/react-form";
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Link, useNavigate } from "@tanstack/react-router";
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Badge } from "@reactive-resume/ui/components/badge";
|
import { Badge } from "@reactive-resume/ui/components/badge";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
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 { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
import { Spinner } from "@reactive-resume/ui/components/spinner";
|
import { Spinner } from "@reactive-resume/ui/components/spinner";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { Combobox } from "@/components/ui/combobox";
|
import { Combobox } from "@/components/ui/combobox";
|
||||||
import { useHasUsableAiProvider } from "@/features/settings/integrations/hooks/use-has-usable-ai-provider";
|
import { useHasUsableAiProvider } from "@/features/settings/integrations/hooks/use-has-usable-ai-provider";
|
||||||
import { useConfirm } from "@/hooks/use-confirm";
|
import { useConfirm } from "@/hooks/use-confirm";
|
||||||
@@ -141,7 +141,9 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) {
|
|||||||
|
|
||||||
setIsImporting(true);
|
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.`,
|
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 });
|
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();
|
closeDialog();
|
||||||
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
|
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getOrpcErrorMessage(error, {
|
type: "error",
|
||||||
|
title: null,
|
||||||
|
description: getOrpcErrorMessage(error, {
|
||||||
byCode: {
|
byCode: {
|
||||||
BAD_REQUEST: t({
|
BAD_REQUEST: t({
|
||||||
comment: "Error shown when AI parsing returns invalid resume structure during import",
|
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.",
|
message: "An unknown error occurred while importing your resume.",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
{ id: toastId, description: null },
|
id: toastId,
|
||||||
);
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useStore } from "@tanstack/react-form";
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { ButtonGroup } from "@reactive-resume/ui/components/button-group";
|
import { ButtonGroup } from "@reactive-resume/ui/components/button-group";
|
||||||
@@ -32,6 +31,7 @@ import {
|
|||||||
InputGroupInput,
|
InputGroupInput,
|
||||||
InputGroupText,
|
InputGroupText,
|
||||||
} from "@reactive-resume/ui/components/input-group";
|
} 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 { generateId, generateRandomName, slugify } from "@reactive-resume/utils/string";
|
||||||
import { ChipInput } from "@/components/input/chip-input";
|
import { ChipInput } from "@/components/input/chip-input";
|
||||||
import { usePatchResume } from "@/features/resume/builder/draft";
|
import { usePatchResume } from "@/features/resume/builder/draft";
|
||||||
@@ -75,17 +75,17 @@ export function CreateResumeDialog(_: DialogProps<"resume.create">) {
|
|||||||
},
|
},
|
||||||
validators: { onSubmit: formSchema },
|
validators: { onSubmit: formSchema },
|
||||||
onSubmit: ({ value }) => {
|
onSubmit: ({ value }) => {
|
||||||
const toastId = toast.loading(t`Creating your resume...`);
|
const toastId = toast.add({ type: "loading", description: t`Creating your resume...` });
|
||||||
|
|
||||||
createResume(value, {
|
createResume(value, {
|
||||||
onSuccess: (id) => {
|
onSuccess: (id) => {
|
||||||
didCreateRef.current = true;
|
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();
|
closeDialog();
|
||||||
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
|
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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,
|
withSampleData: true,
|
||||||
} satisfies RouterInput["resume"]["create"];
|
} 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, {
|
createResume(data, {
|
||||||
onSuccess: (id) => {
|
onSuccess: (id) => {
|
||||||
didCreateRef.current = true;
|
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();
|
closeDialog();
|
||||||
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
|
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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 },
|
validators: { onSubmit: formSchema },
|
||||||
onSubmit: ({ value }) => {
|
onSubmit: ({ value }) => {
|
||||||
const toastId = toast.loading(t`Updating your resume...`);
|
const toastId = toast.add({ type: "loading", description: t`Updating your resume...` });
|
||||||
|
|
||||||
updateResume(value, {
|
updateResume(value, {
|
||||||
onSuccess: (updated) => {
|
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();
|
closeDialog();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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 },
|
validators: { onSubmit: formSchema },
|
||||||
onSubmit: ({ value }) => {
|
onSubmit: ({ value }) => {
|
||||||
const toastId = toast.loading(t`Duplicating your resume...`);
|
const toastId = toast.add({ type: "loading", description: t`Duplicating your resume...` });
|
||||||
|
|
||||||
duplicateResume(value, {
|
duplicateResume(value, {
|
||||||
onSuccess: (id) => {
|
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();
|
closeDialog();
|
||||||
|
|
||||||
if (!data.shouldRedirect) return;
|
if (!data.shouldRedirect) return;
|
||||||
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
|
void navigate({ to: "/builder/$resumeId", params: { resumeId: id } });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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 { t } from "@lingui/core/macro";
|
||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { SlideshowIcon } from "@phosphor-icons/react";
|
import { SlideshowIcon } from "@phosphor-icons/react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Badge } from "@reactive-resume/ui/components/badge";
|
import { Badge } from "@reactive-resume/ui/components/badge";
|
||||||
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@reactive-resume/ui/components/dialog";
|
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@reactive-resume/ui/components/dialog";
|
||||||
import { ScrollArea } from "@reactive-resume/ui/components/scroll-area";
|
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 { cn } from "@reactive-resume/utils/style";
|
||||||
import { CometCard } from "@/components/animation/comet-card";
|
import { CometCard } from "@/components/animation/comet-card";
|
||||||
import { useDialogStore } from "@/dialogs/store";
|
import { useDialogStore } from "@/dialogs/store";
|
||||||
@@ -33,9 +33,10 @@ export function TemplateGalleryDialog(_: DialogProps<"resume.template.gallery">)
|
|||||||
|
|
||||||
closeDialog();
|
closeDialog();
|
||||||
|
|
||||||
toast(t`Switched to the ${templates[template].name} template.`, {
|
toast.add({
|
||||||
action: {
|
description: t`Switched to the ${templates[template].name} template.`,
|
||||||
label: t`Undo`,
|
actionProps: {
|
||||||
|
children: t`Undo`,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
updateResumeData((draft) => {
|
updateResumeData((draft) => {
|
||||||
draft.metadata.template = previousTemplate;
|
draft.metadata.template = previousTemplate;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
TrayArrowUpIcon,
|
TrayArrowUpIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { STAGES } from "@reactive-resume/schema/applications/data";
|
import { STAGES } from "@reactive-resume/schema/applications/data";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +22,7 @@ import {
|
|||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@reactive-resume/ui/components/dropdown-menu";
|
} from "@reactive-resume/ui/components/dropdown-menu";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { cn } from "@reactive-resume/utils/style";
|
import { cn } from "@reactive-resume/utils/style";
|
||||||
import { useConfirm } from "@/hooks/use-confirm";
|
import { useConfirm } from "@/hooks/use-confirm";
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
@@ -55,7 +55,7 @@ export function ApplicationActionsMenu({ application, onEdit, showOnHover, class
|
|||||||
const update = useMutation(
|
const update = useMutation(
|
||||||
orpc.applications.update.mutationOptions({
|
orpc.applications.update.mutationOptions({
|
||||||
onSuccess: invalidate,
|
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({
|
orpc.applications.delete.mutationOptions({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
invalidate();
|
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";
|
} from "@phosphor-icons/react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { cn } from "@reactive-resume/utils/style";
|
import { cn } from "@reactive-resume/utils/style";
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
import { applicationsListQueryKey } from "../queries";
|
import { applicationsListQueryKey } from "../queries";
|
||||||
@@ -113,22 +113,22 @@ export function ApplicationAiCopilot({ application }: Props) {
|
|||||||
const matchScore = useMutation(
|
const matchScore = useMutation(
|
||||||
orpc.applications.ai.matchScore.mutationOptions({
|
orpc.applications.ai.matchScore.mutationOptions({
|
||||||
onSuccess: invalidate,
|
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(
|
const tailorResume = useMutation(
|
||||||
orpc.applications.ai.tailorResume.mutationOptions({
|
orpc.applications.ai.tailorResume.mutationOptions({
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
invalidate();
|
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(
|
const draftMessage = useMutation(
|
||||||
orpc.applications.ai.draftMessage.mutationOptions({
|
orpc.applications.ai.draftMessage.mutationOptions({
|
||||||
onSuccess: (result, variables) => setDraft({ kind: variables.kind, text: result.text }),
|
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">
|
<div className="px-3.5 py-3">
|
||||||
{!canScore ? (
|
{!canScore ? (
|
||||||
<p className="rounded-lg bg-muted/50 p-2.5 text-muted-foreground text-xs">
|
<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>
|
</p>
|
||||||
) : score == null ? (
|
) : score == null ? (
|
||||||
<button
|
<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"
|
className="inline-flex items-center gap-1 text-muted-foreground text-xs hover:text-foreground"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void navigator.clipboard.writeText(draft.text);
|
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>
|
<CopyIcon className="size-3.5" /> <Trans>Copy</Trans>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { STAGES } from "@reactive-resume/schema/applications/data";
|
import { STAGES } from "@reactive-resume/schema/applications/data";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +27,7 @@ import {
|
|||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@reactive-resume/ui/components/sheet";
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@reactive-resume/ui/components/sheet";
|
||||||
import { Textarea } from "@reactive-resume/ui/components/textarea";
|
import { Textarea } from "@reactive-resume/ui/components/textarea";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { cn } from "@reactive-resume/utils/style";
|
import { cn } from "@reactive-resume/utils/style";
|
||||||
import { useConfirm } from "@/hooks/use-confirm";
|
import { useConfirm } from "@/hooks/use-confirm";
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
@@ -83,28 +83,28 @@ export function ApplicationDetailSheet({ application, onOpenChange, onEdit }: Pr
|
|||||||
const update = useMutation(
|
const update = useMutation(
|
||||||
orpc.applications.update.mutationOptions({
|
orpc.applications.update.mutationOptions({
|
||||||
onSuccess: invalidate,
|
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(
|
const addNote = useMutation(
|
||||||
orpc.applications.addNote.mutationOptions({
|
orpc.applications.addNote.mutationOptions({
|
||||||
onSuccess: invalidate,
|
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(
|
const updateTimelineEntry = useMutation(
|
||||||
orpc.applications.updateTimelineEntry.mutationOptions({
|
orpc.applications.updateTimelineEntry.mutationOptions({
|
||||||
onSuccess: invalidate,
|
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(
|
const deleteTimelineEntry = useMutation(
|
||||||
orpc.applications.deleteTimelineEntry.mutationOptions({
|
orpc.applications.deleteTimelineEntry.mutationOptions({
|
||||||
onSuccess: invalidate,
|
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({
|
orpc.applications.delete.mutationOptions({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
invalidate();
|
invalidate();
|
||||||
toast.success(t`Application deleted.`);
|
toast.add({ type: "success", description: t`Application deleted.` });
|
||||||
onOpenChange(false);
|
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 { t } from "@lingui/core/macro";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { STAGES } from "@reactive-resume/schema/applications/data";
|
import { STAGES } from "@reactive-resume/schema/applications/data";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { cn } from "@reactive-resume/utils/style";
|
import { cn } from "@reactive-resume/utils/style";
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
import { applicationsListQueryKey } from "../queries";
|
import { applicationsListQueryKey } from "../queries";
|
||||||
@@ -48,7 +48,7 @@ export function ApplicationBoard({ applications, onOpen, onEdit }: Props) {
|
|||||||
},
|
},
|
||||||
onError: (_error, _vars, context) => {
|
onError: (_error, _vars, context) => {
|
||||||
if (context?.previous) queryClient.setQueryData(listKey, context.previous);
|
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 }),
|
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 { FilePdfIcon, UploadSimpleIcon, XIcon } from "@phosphor-icons/react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
|
|
||||||
export type FileAttachment = { url: string; name: string };
|
export type FileAttachment = { url: string; name: string };
|
||||||
@@ -28,17 +28,18 @@ export function FileAttachmentField({ value, onChange, attachLabel, disabled }:
|
|||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
if (file.type !== "application/pdf") {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const toastId = toast.loading(t`Uploading…`);
|
const toastId = toast.add({ type: "loading", description: t`Uploading…` });
|
||||||
upload.mutate(file, {
|
upload.mutate(file, {
|
||||||
onSuccess: ({ url }) => {
|
onSuccess: ({ url }) => {
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
onChange({ url, name: file.name });
|
onChange({ url, name: file.name });
|
||||||
if (inputRef.current) inputRef.current.value = "";
|
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 { CheckCircleIcon, UploadSimpleIcon } from "@phosphor-icons/react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useMemo, useRef, useState } from "react";
|
import { useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Badge } from "@reactive-resume/ui/components/badge";
|
import { Badge } from "@reactive-resume/ui/components/badge";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { Label } from "@reactive-resume/ui/components/label";
|
import { Label } from "@reactive-resume/ui/components/label";
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
SheetTitle,
|
SheetTitle,
|
||||||
} from "@reactive-resume/ui/components/sheet";
|
} from "@reactive-resume/ui/components/sheet";
|
||||||
import { Textarea } from "@reactive-resume/ui/components/textarea";
|
import { Textarea } from "@reactive-resume/ui/components/textarea";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
import { mapCsvToApplications, parseCsv } from "../csv";
|
import { mapCsvToApplications, parseCsv } from "../csv";
|
||||||
import { applicationsListQueryKey } from "../queries";
|
import { applicationsListQueryKey } from "../queries";
|
||||||
@@ -51,12 +51,12 @@ export function ImportApplicationsSheet({ open, onOpenChange }: Props) {
|
|||||||
void queryClient.invalidateQueries({ queryKey: applicationsListQueryKey() });
|
void queryClient.invalidateQueries({ queryKey: applicationsListQueryKey() });
|
||||||
void queryClient.invalidateQueries({ queryKey: orpc.applications.stats.queryKey() });
|
void queryClient.invalidateQueries({ queryKey: orpc.applications.stats.queryKey() });
|
||||||
void queryClient.invalidateQueries({ queryKey: orpc.applications.tags.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("");
|
setText("");
|
||||||
resetFile();
|
resetFile();
|
||||||
onOpenChange(false);
|
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 { DownloadSimpleIcon } from "@phosphor-icons/react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
import { computeInsights, computeTimeline } from "../insights";
|
import { computeInsights, computeTimeline } from "../insights";
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ function PipelineFlow({ insights }: { insights: ReturnType<typeof computeInsight
|
|||||||
link.download = "pipeline-flow.png";
|
link.download = "pipeline-flow.png";
|
||||||
link.href = canvas.toDataURL("image/png");
|
link.href = canvas.toDataURL("image/png");
|
||||||
link.click();
|
link.click();
|
||||||
toast.success(t`Exported pipeline-flow.png`);
|
toast.add({ type: "success", description: t`Exported pipeline-flow.png` });
|
||||||
};
|
};
|
||||||
image.src = svg64;
|
image.src = svg64;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Trans } from "@lingui/react/macro";
|
|||||||
import { ArchiveIcon, ArrowRightIcon, TagIcon, TrashIcon } from "@phosphor-icons/react";
|
import { ArchiveIcon, ArrowRightIcon, TagIcon, TrashIcon } from "@phosphor-icons/react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { STAGES } from "@reactive-resume/schema/applications/data";
|
import { STAGES } from "@reactive-resume/schema/applications/data";
|
||||||
import { Badge } from "@reactive-resume/ui/components/badge";
|
import { Badge } from "@reactive-resume/ui/components/badge";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
@@ -18,6 +17,7 @@ import {
|
|||||||
} from "@reactive-resume/ui/components/dropdown-menu";
|
} from "@reactive-resume/ui/components/dropdown-menu";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@reactive-resume/ui/components/popover";
|
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 { getInitials } from "@reactive-resume/utils/string";
|
||||||
import { cn } from "@reactive-resume/utils/style";
|
import { cn } from "@reactive-resume/utils/style";
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
@@ -70,7 +70,7 @@ export function ApplicationTable({ applications, onOpen, onEdit }: Props) {
|
|||||||
invalidate();
|
invalidate();
|
||||||
clearSelection();
|
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) => {
|
onSuccess: (result) => {
|
||||||
invalidate();
|
invalidate();
|
||||||
clearSelection();
|
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 { FingerprintIcon, GithubLogoIcon, GoogleLogoIcon, LinkedinLogoIcon, VaultIcon } from "@phosphor-icons/react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { Skeleton } from "@reactive-resume/ui/components/skeleton";
|
import { Skeleton } from "@reactive-resume/ui/components/skeleton";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { cn } from "@reactive-resume/utils/style";
|
import { cn } from "@reactive-resume/utils/style";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
@@ -50,20 +50,22 @@ function SocialAuthButtons({ providers }: SocialAuthButtonsProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const runSignIn = async (fn: () => Promise<{ error: { message?: string } | null }>) => {
|
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();
|
const { error } = await fn();
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
error.message ||
|
type: "error",
|
||||||
|
description:
|
||||||
|
error.message ||
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when sign-in fails without an error message",
|
comment: "Fallback toast when sign-in fails without an error message",
|
||||||
message: "Failed to sign in. Please try again.",
|
message: "Failed to sign in. Please try again.",
|
||||||
}),
|
}),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
await router.invalidate();
|
await router.invalidate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { Trans } from "@lingui/react/macro";
|
|||||||
import { ArrowRightIcon } from "@phosphor-icons/react";
|
import { ArrowRightIcon } from "@phosphor-icons/react";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { useAppForm } from "@/libs/tanstack-form";
|
import { useAppForm } from "@/libs/tanstack-form";
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export function ForgotPasswordPage() {
|
|||||||
defaultValues: { email: "" },
|
defaultValues: { email: "" },
|
||||||
validators: { onSubmit: formSchema },
|
validators: { onSubmit: formSchema },
|
||||||
onSubmit: async ({ value }) => {
|
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({
|
const { error } = await authClient.requestPasswordReset({
|
||||||
email: value.email,
|
email: value.email,
|
||||||
@@ -30,19 +30,21 @@ export function ForgotPasswordPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
error.message ||
|
type: "error",
|
||||||
|
description:
|
||||||
|
error.message ||
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when requesting password reset email fails without backend message",
|
comment: "Fallback toast when requesting password reset email fails without backend message",
|
||||||
message: "Failed to send password reset email. Please try again.",
|
message: "Failed to send password reset email. Please try again.",
|
||||||
}),
|
}),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSubmitted(true);
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
import { Link, useNavigate, useRouter } from "@tanstack/react-router";
|
import { Link, useNavigate, useRouter } from "@tanstack/react-router";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useToggle } from "usehooks-ts";
|
import { useToggle } from "usehooks-ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
import { useAppForm } from "@/libs/tanstack-form";
|
import { useAppForm } from "@/libs/tanstack-form";
|
||||||
@@ -38,7 +38,7 @@ export function LoginPage({ disableEmailAuth, disableSignups }: Props) {
|
|||||||
defaultValues: { identifier: "", password: "" },
|
defaultValues: { identifier: "", password: "" },
|
||||||
validators: { onSubmit: formSchema },
|
validators: { onSubmit: formSchema },
|
||||||
onSubmit: async ({ value }) => {
|
onSubmit: async ({ value }) => {
|
||||||
const toastId = toast.loading(t`Signing in...`);
|
const toastId = toast.add({ type: "loading", description: t`Signing in...` });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isEmail = value.identifier.includes("@");
|
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 });
|
: await authClient.signIn.username({ username: value.identifier, password: value.password });
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
result.error.message ||
|
type: "error",
|
||||||
|
description:
|
||||||
|
result.error.message ||
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when sign-in fails and no server error message is available",
|
comment: "Fallback toast when sign-in fails and no server error message is available",
|
||||||
message: "Failed to sign in. Please try again.",
|
message: "Failed to sign in. Please try again.",
|
||||||
}),
|
}),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,16 +68,16 @@ export function LoginPage({ disableEmailAuth, disableSignups }: Props) {
|
|||||||
result.data.twoFactorRedirect;
|
result.data.twoFactorRedirect;
|
||||||
|
|
||||||
if (requiresTwoFactor) {
|
if (requiresTwoFactor) {
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
void navigate({ to: "/auth/verify-2fa", replace: true });
|
void navigate({ to: "/auth/verify-2fa", replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
await router.invalidate();
|
await router.invalidate();
|
||||||
void navigate({ to: "/dashboard", replace: true });
|
void navigate({ to: "/dashboard", replace: true });
|
||||||
} catch {
|
} 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 { ArrowRightIcon, EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useToggle } from "usehooks-ts";
|
import { useToggle } from "usehooks-ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@reactive-resume/ui/components/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@reactive-resume/ui/components/alert";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { useAppForm } from "@/libs/tanstack-form";
|
import { useAppForm } from "@/libs/tanstack-form";
|
||||||
import { SocialAuth } from "../components/social-auth";
|
import { SocialAuth } from "../components/social-auth";
|
||||||
@@ -41,7 +41,7 @@ export function RegisterPage({ disableEmailAuth }: Props) {
|
|||||||
defaultValues: { name: "", username: "", email: "", password: "" },
|
defaultValues: { name: "", username: "", email: "", password: "" },
|
||||||
validators: { onSubmit: formSchema },
|
validators: { onSubmit: formSchema },
|
||||||
onSubmit: async ({ value }) => {
|
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({
|
const { error } = await authClient.signUp.email({
|
||||||
name: value.name,
|
name: value.name,
|
||||||
@@ -53,19 +53,21 @@ export function RegisterPage({ disableEmailAuth }: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
error.message ||
|
type: "error",
|
||||||
|
description:
|
||||||
|
error.message ||
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when account registration fails without a server error message",
|
comment: "Fallback toast when account registration fails without a server error message",
|
||||||
message: "Failed to create your account. Please try again.",
|
message: "Failed to create your account. Please try again.",
|
||||||
}),
|
}),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSubmitted(true);
|
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 { Trans } from "@lingui/react/macro";
|
||||||
import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
|
import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useToggle } from "usehooks-ts";
|
import { useToggle } from "usehooks-ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { useAppForm } from "@/libs/tanstack-form";
|
import { useAppForm } from "@/libs/tanstack-form";
|
||||||
|
|
||||||
@@ -27,23 +27,27 @@ export function ResetPasswordPage({ token }: Props) {
|
|||||||
defaultValues: { password: "" },
|
defaultValues: { password: "" },
|
||||||
validators: { onSubmit: formSchema },
|
validators: { onSubmit: formSchema },
|
||||||
onSubmit: async ({ value }) => {
|
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 });
|
const { error } = await authClient.resetPassword({ token, newPassword: value.password });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
error.message ||
|
type: "error",
|
||||||
|
description:
|
||||||
|
error.message ||
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when resetting password fails and no backend message is available",
|
comment: "Fallback toast when resetting password fails and no backend message is available",
|
||||||
message: "Failed to reset your password. Please try again.",
|
message: "Failed to reset your password. Please try again.",
|
||||||
}),
|
}),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
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,
|
id: toastId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { ORPCError } from "@orpc/client";
|
|||||||
import { EyeIcon, EyeSlashIcon, LockOpenIcon } from "@phosphor-icons/react";
|
import { EyeIcon, EyeSlashIcon, LockOpenIcon } from "@phosphor-icons/react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useToggle } from "usehooks-ts";
|
import { useToggle } from "usehooks-ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { getReadableErrorMessage } from "@/libs/error-message";
|
import { getReadableErrorMessage } from "@/libs/error-message";
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
import { useAppForm } from "@/libs/tanstack-form";
|
import { useAppForm } from "@/libs/tanstack-form";
|
||||||
@@ -35,18 +35,18 @@ export function ResumePasswordPage({ redirectPath }: Props) {
|
|||||||
defaultValues: { password: "" },
|
defaultValues: { password: "" },
|
||||||
validators: { onSubmit: formSchema },
|
validators: { onSubmit: formSchema },
|
||||||
onSubmit: ({ value, formApi }) => {
|
onSubmit: ({ value, formApi }) => {
|
||||||
const toastId = toast.loading(t`Verifying password...`);
|
const toastId = toast.add({ type: "loading", description: t`Verifying password...` });
|
||||||
|
|
||||||
verifyPassword(
|
verifyPassword(
|
||||||
{ username, slug, password: value.password },
|
{ username, slug, password: value.password },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
void navigate({ to: redirectPath, replace: true });
|
void navigate({ to: redirectPath, replace: true });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (error instanceof ORPCError && error.code === "INVALID_PASSWORD") {
|
if (error instanceof ORPCError && error.code === "INVALID_PASSWORD") {
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
formApi.setFieldMeta("password", (meta) => ({
|
formApi.setFieldMeta("password", (meta) => ({
|
||||||
...meta,
|
...meta,
|
||||||
isTouched: true,
|
isTouched: true,
|
||||||
@@ -57,16 +57,17 @@ export function ResumePasswordPage({ redirectPath }: Props) {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when resume password verification fails unexpectedly",
|
comment: "Fallback toast when resume password verification fails unexpectedly",
|
||||||
message: "Failed to verify the password. Please try again.",
|
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 { Trans } from "@lingui/react/macro";
|
||||||
import { ArrowLeftIcon, CheckIcon } from "@phosphor-icons/react";
|
import { ArrowLeftIcon, CheckIcon } from "@phosphor-icons/react";
|
||||||
import { Link, useNavigate, useRouter } from "@tanstack/react-router";
|
import { Link, useNavigate, useRouter } from "@tanstack/react-router";
|
||||||
import { toast } from "sonner";
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { FormControl, FormItem, FormMessage } from "@reactive-resume/ui/components/form";
|
import { FormControl, FormItem, FormMessage } from "@reactive-resume/ui/components/form";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { useAppForm } from "@/libs/tanstack-form";
|
import { useAppForm } from "@/libs/tanstack-form";
|
||||||
|
|
||||||
@@ -30,15 +30,20 @@ function TwoFactorVerificationPage({ backupCode = false }: TwoFactorVerification
|
|||||||
defaultValues: { code: "" },
|
defaultValues: { code: "" },
|
||||||
validators: { onSubmit: backupCode ? backupCodeSchema : totpSchema },
|
validators: { onSubmit: backupCode ? backupCodeSchema : totpSchema },
|
||||||
onSubmit: async ({ value }) => {
|
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 code = backupCode ? `${value.code.slice(0, 5)}-${value.code.slice(5)}` : value.code;
|
||||||
const { error } = backupCode
|
const { error } = backupCode
|
||||||
? await authClient.twoFactor.verifyBackupCode({ code })
|
? await authClient.twoFactor.verifyBackupCode({ code })
|
||||||
: await authClient.twoFactor.verifyTotp({ code });
|
: await authClient.twoFactor.verifyTotp({ code });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
error.message ||
|
type: "error",
|
||||||
|
description:
|
||||||
|
error.message ||
|
||||||
(backupCode
|
(backupCode
|
||||||
? t({
|
? t({
|
||||||
comment: "Fallback toast when verifying a backup two-factor authentication code fails",
|
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",
|
comment: "Fallback toast when verifying a two-factor authentication code fails",
|
||||||
message: "Failed to verify your code. Please try again.",
|
message: "Failed to verify your code. Please try again.",
|
||||||
})),
|
})),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
await router.invalidate();
|
await router.invalidate();
|
||||||
void navigate({ to: "/dashboard", replace: true });
|
void navigate({ to: "/dashboard", replace: true });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ const routerParamsMock = vi.hoisted(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const toastMocks = vi.hoisted(() => ({
|
const toastMocks = vi.hoisted(() => ({
|
||||||
dismiss: vi.fn(),
|
add: vi.fn(() => "sync-error-toast"),
|
||||||
error: vi.fn(() => "sync-error-toast"),
|
close: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@orpc/client", () => ({
|
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,
|
toast: toastMocks,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -125,8 +125,8 @@ describe("builder resume autosave", () => {
|
|||||||
queryClientMock.setQueryData.mockClear();
|
queryClientMock.setQueryData.mockClear();
|
||||||
routerParamsMock.value = {};
|
routerParamsMock.value = {};
|
||||||
i18n.loadAndActivate({ locale: "en-US", messages: {} });
|
i18n.loadAndActivate({ locale: "en-US", messages: {} });
|
||||||
toastMocks.dismiss.mockClear();
|
toastMocks.add.mockClear();
|
||||||
toastMocks.error.mockClear();
|
toastMocks.close.mockClear();
|
||||||
useResumeStore.getState().reset();
|
useResumeStore.getState().reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -268,9 +268,8 @@ describe("builder resume autosave", () => {
|
|||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
expect(useResumeStore.getState().resume?.data.basics.name).toBe("Unsaved Name");
|
expect(useResumeStore.getState().resume?.data.basics.name).toBe("Unsaved Name");
|
||||||
expect(toastMocks.error).toHaveBeenCalledWith(
|
expect(toastMocks.add).toHaveBeenCalledWith(
|
||||||
"Your latest changes could not be saved.",
|
expect.objectContaining({ type: "error", description: "Your latest changes could not be saved.", timeout: 0 }),
|
||||||
expect.objectContaining({ duration: Number.POSITIVE_INFINITY }),
|
|
||||||
);
|
);
|
||||||
expect(orpcMocks.patchResume).not.toHaveBeenCalled();
|
expect(orpcMocks.patchResume).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
import { useParams } from "@tanstack/react-router";
|
import { useParams } from "@tanstack/react-router";
|
||||||
import { debounce, isEqual } from "es-toolkit";
|
import { debounce, isEqual } from "es-toolkit";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { immer } from "zustand/middleware/immer";
|
import { immer } from "zustand/middleware/immer";
|
||||||
import { create } from "zustand/react";
|
import { create } from "zustand/react";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { orpc, streamClient } from "@/libs/orpc/client";
|
import { orpc, streamClient } from "@/libs/orpc/client";
|
||||||
|
|
||||||
export type Resume = {
|
export type Resume = {
|
||||||
@@ -63,7 +63,7 @@ type Runtime = {
|
|||||||
hasPendingLocalChanges: boolean;
|
hasPendingLocalChanges: boolean;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
pendingResume?: Resume;
|
pendingResume?: Resume;
|
||||||
syncErrorToastId?: string | number;
|
syncErrorToastId?: string;
|
||||||
syncResume: ReturnType<typeof debounce<(resume: Resume) => void>>;
|
syncResume: ReturnType<typeof debounce<(resume: Resume) => void>>;
|
||||||
beforeUnloadHandler?: () => void;
|
beforeUnloadHandler?: () => void;
|
||||||
deferredRemoteResume?: Resume;
|
deferredRemoteResume?: Resume;
|
||||||
@@ -92,7 +92,7 @@ function resetHistoryRuntime() {
|
|||||||
historyCanCoalesce = false;
|
historyCanCoalesce = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lockedToastId: string | number | undefined;
|
let lockedToastId: string | undefined;
|
||||||
|
|
||||||
function getResumeQueryKey(id: string): QueryKey {
|
function getResumeQueryKey(id: string): QueryKey {
|
||||||
return orpc.resume.getById.queryOptions({ input: { id } }).queryKey as QueryKey;
|
return orpc.resume.getById.queryOptions({ input: { id } }).queryKey as QueryKey;
|
||||||
@@ -126,7 +126,7 @@ function externalUpdateMessage(mutation: ResumeUpdateMutation): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function notifyExternalUpdate(mutation: ResumeUpdateMutation) {
|
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.
|
// #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) {
|
if (runtime.syncErrorToastId !== undefined) {
|
||||||
toast.dismiss(runtime.syncErrorToastId);
|
toast.close(runtime.syncErrorToastId);
|
||||||
runtime.syncErrorToastId = undefined;
|
runtime.syncErrorToastId = undefined;
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@@ -223,9 +223,11 @@ async function flushResumeSave(id: string) {
|
|||||||
runtime.pendingResume ??= submitted;
|
runtime.pendingResume ??= submitted;
|
||||||
runtime.hasPendingLocalChanges = true;
|
runtime.hasPendingLocalChanges = true;
|
||||||
useResumeStore.getState().setSaveStatus("error");
|
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,
|
id: runtime.syncErrorToastId,
|
||||||
duration: Number.POSITIVE_INFINITY,
|
timeout: 0,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
runtime.isSaving = false;
|
runtime.isSaving = false;
|
||||||
@@ -417,7 +419,9 @@ export const useResumeStore = create<ResumeStore>()(
|
|||||||
if (!currentResume) return;
|
if (!currentResume) return;
|
||||||
|
|
||||||
if (currentResume.isLocked) {
|
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,
|
id: lockedToastId,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -472,7 +476,11 @@ function applyHistoryStep(get: StoreGet, set: ImmerSet, direction: "undo" | "red
|
|||||||
if (!currentResume) return;
|
if (!currentResume) return;
|
||||||
|
|
||||||
if (currentResume.isLocked) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const mocks = vi.hoisted(() => ({
|
|||||||
createResumePdfBlob: vi.fn(async () => new Blob(["local"], { type: "application/pdf" })),
|
createResumePdfBlob: vi.fn(async () => new Blob(["local"], { type: "application/pdf" })),
|
||||||
downloadWithAnchor: vi.fn(),
|
downloadWithAnchor: vi.fn(),
|
||||||
fetch: vi.fn(async (_input: string | URL) => new Response(new Blob(["server"], { type: "application/pdf" }))),
|
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", () => ({
|
vi.mock("@/features/resume/export/pdf-document", () => ({
|
||||||
@@ -20,11 +20,10 @@ vi.mock("@reactive-resume/utils/file", () => ({
|
|||||||
downloadWithAnchor: mocks.downloadWithAnchor,
|
downloadWithAnchor: mocks.downloadWithAnchor,
|
||||||
generateFilename: (name: string, extension: string) => `${name}.${extension}`,
|
generateFilename: (name: string, extension: string) => `${name}.${extension}`,
|
||||||
}));
|
}));
|
||||||
vi.mock("sonner", () => ({
|
vi.mock("@reactive-resume/ui/components/toast", () => ({
|
||||||
toast: {
|
toast: {
|
||||||
loading: vi.fn(() => "toast"),
|
add: mocks.toastAdd,
|
||||||
error: mocks.toastError,
|
close: vi.fn(),
|
||||||
dismiss: vi.fn(),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ beforeEach(() => {
|
|||||||
mocks.createResumePdfBlob.mockClear();
|
mocks.createResumePdfBlob.mockClear();
|
||||||
mocks.downloadWithAnchor.mockClear();
|
mocks.downloadWithAnchor.mockClear();
|
||||||
mocks.fetch.mockClear();
|
mocks.fetch.mockClear();
|
||||||
mocks.toastError.mockClear();
|
mocks.toastAdd.mockClear();
|
||||||
vi.stubGlobal("fetch", mocks.fetch);
|
vi.stubGlobal("fetch", mocks.fetch);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,6 +69,6 @@ describe("useResumeExport public PDF", () => {
|
|||||||
await act(() => result.current.onDownloadPDF());
|
await act(() => result.current.onDownloadPDF());
|
||||||
|
|
||||||
expect(mocks.downloadWithAnchor).not.toHaveBeenCalled();
|
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 type { PublicResumePdfOptions } from "@/features/resume/public/public-pdf";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { buildDocx } from "@reactive-resume/docx";
|
import { buildDocx } from "@reactive-resume/docx";
|
||||||
import { getResumeSectionTitle } from "@reactive-resume/pdf/section-title";
|
import { getResumeSectionTitle } from "@reactive-resume/pdf/section-title";
|
||||||
import { getResumeExportData, resumeHasCoverLetter } from "@reactive-resume/resume/export-sections";
|
import { getResumeExportData, resumeHasCoverLetter } from "@reactive-resume/resume/export-sections";
|
||||||
import { buildMarkdown } from "@reactive-resume/resume/markdown";
|
import { buildMarkdown } from "@reactive-resume/resume/markdown";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
|
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
|
||||||
import { resolvePublicResumePdfBlob } from "@/features/resume/public/public-pdf";
|
import { resolvePublicResumePdfBlob } from "@/features/resume/public/public-pdf";
|
||||||
import { createSectionTitleResolverForLocale } from "@/libs/resume/section-title-locale";
|
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);
|
const blob = await buildDocx(data, resolveTitle);
|
||||||
downloadWithAnchor(blob, generateFilename(getTargetExportName(resume, target), "docx"));
|
downloadWithAnchor(blob, generateFilename(getTargetExportName(resume, target), "docx"));
|
||||||
} catch {
|
} 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],
|
[resume],
|
||||||
@@ -90,7 +90,10 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
|
|||||||
async (target: ResumeExportTarget = "resume", downloadOptions?: DownloadPdfOptions) => {
|
async (target: ResumeExportTarget = "resume", downloadOptions?: DownloadPdfOptions) => {
|
||||||
if (!resume) return;
|
if (!resume) return;
|
||||||
if (target === "cover-letter" && !resumeHasCoverLetter(resume.data)) 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);
|
setIsExporting(true);
|
||||||
try {
|
try {
|
||||||
const data = exportOptions.publicResumePdf ? resume.data : getResumeExportData(resume.data, target);
|
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"));
|
downloadWithAnchor(blob, generateFilename(getTargetExportName(resume, target), "pdf"));
|
||||||
} catch {
|
} 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 {
|
} finally {
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[exportOptions.publicResumePdf, resume],
|
[exportOptions.publicResumePdf, resume],
|
||||||
@@ -116,7 +119,7 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
|
|||||||
|
|
||||||
const onPrint = useCallback(async () => {
|
const onPrint = useCallback(async () => {
|
||||||
if (!resume) return;
|
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);
|
setIsExporting(true);
|
||||||
try {
|
try {
|
||||||
const blob = exportOptions.publicResumePdf
|
const blob = exportOptions.publicResumePdf
|
||||||
@@ -142,10 +145,13 @@ export function useResumeExport(resume: ExportableResume | undefined, exportOpti
|
|||||||
};
|
};
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
} catch {
|
} 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 {
|
} finally {
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
}
|
}
|
||||||
}, [exportOptions.publicResumePdf, resume]);
|
}, [exportOptions.publicResumePdf, resume]);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { ResumePreviewClient } from "./preview.browser";
|
|||||||
|
|
||||||
const previewMock = vi.hoisted(() => ({
|
const previewMock = vi.hoisted(() => ({
|
||||||
builderResumeData: undefined as ResumeData | undefined,
|
builderResumeData: undefined as ResumeData | undefined,
|
||||||
toastError: vi.fn(),
|
toastAdd: vi.fn(),
|
||||||
toBlob: vi.fn(async () => new Blob(["%PDF"], { type: "application/pdf" })),
|
toBlob: vi.fn(async () => new Blob(["%PDF"], { type: "application/pdf" })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -40,8 +40,8 @@ vi.mock("@/features/resume/export/pdf-document", () => ({
|
|||||||
createResumePdfBlob: previewMock.toBlob,
|
createResumePdfBlob: previewMock.toBlob,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("sonner", () => ({
|
vi.mock("@reactive-resume/ui/components/toast", () => ({
|
||||||
toast: { error: previewMock.toastError },
|
toast: { add: previewMock.toastAdd },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../builder/draft", () => ({
|
vi.mock("../builder/draft", () => ({
|
||||||
@@ -85,7 +85,7 @@ describe("ResumePreviewClient", () => {
|
|||||||
previewMock.builderResumeData = undefined;
|
previewMock.builderResumeData = undefined;
|
||||||
previewMock.toBlob.mockReset();
|
previewMock.toBlob.mockReset();
|
||||||
previewMock.toBlob.mockImplementation(async () => new Blob(["%PDF"], { type: "application/pdf" }));
|
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", () => {
|
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} />);
|
view.rerender(<ResumePreviewClient pageLayout="vertical" pageScale={1.25} showPageNumbers={false} />);
|
||||||
|
|
||||||
await waitFor(() => expect(previewMock.toBlob).toHaveBeenCalledTimes(2));
|
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();
|
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 { t } from "@lingui/core/macro";
|
||||||
import { AnimatePresence, m } from "motion/react";
|
import { AnimatePresence, m } from "motion/react";
|
||||||
import { useEffect, useRef, useState } from "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 { isRTL } from "@reactive-resume/utils/locale";
|
||||||
import { cn } from "@reactive-resume/utils/style";
|
import { cn } from "@reactive-resume/utils/style";
|
||||||
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
|
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
|
||||||
@@ -136,7 +136,9 @@ export function ResumePreviewClient({
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (cancelled || requestId !== requestIdRef.current) return;
|
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",
|
id: "resume-preview-render-error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { match } from "ts-pattern";
|
import { match } from "ts-pattern";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { getReadableErrorMessage } from "@/libs/error-message";
|
import { getReadableErrorMessage } from "@/libs/error-message";
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
@@ -108,48 +108,53 @@ export function useAuthAccounts() {
|
|||||||
export function useAuthProviderActions() {
|
export function useAuthProviderActions() {
|
||||||
const link = useCallback(async (provider: AuthProvider) => {
|
const link = useCallback(async (provider: AuthProvider) => {
|
||||||
const providerName = getProviderName(provider);
|
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" });
|
const { error } = await authClient.linkSocial({ provider, callbackURL: "/dashboard/settings/authentication" });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when linking a social authentication provider fails",
|
comment: "Fallback toast when linking a social authentication provider fails",
|
||||||
message: "Failed to link provider. Please try again.",
|
message: "Failed to link provider. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const unlink = useCallback(async (provider: AuthProvider, accountId: string) => {
|
const unlink = useCallback(async (provider: AuthProvider, accountId: string) => {
|
||||||
const providerName = getProviderName(provider);
|
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 });
|
const { error } = await authClient.unlinkAccount({ providerId: provider, accountId });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when unlinking a social authentication provider fails",
|
comment: "Fallback toast when unlinking a social authentication provider fails",
|
||||||
message: "Failed to unlink provider. Please try again.",
|
message: "Failed to unlink provider. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { link, unlink };
|
return { link, unlink };
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Trans } from "@lingui/react/macro";
|
|||||||
import { KeyIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react";
|
import { KeyIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { m } from "motion/react";
|
import { m } from "motion/react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { Separator } from "@reactive-resume/ui/components/separator";
|
import { Separator } from "@reactive-resume/ui/components/separator";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { usePrompt } from "@/hooks/use-prompt";
|
import { usePrompt } from "@/hooks/use-prompt";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { getReadableErrorMessage } from "@/libs/error-message";
|
import { getReadableErrorMessage } from "@/libs/error-message";
|
||||||
@@ -24,19 +24,20 @@ export function PasskeysSection() {
|
|||||||
mutationFn: () => authClient.passkey.addPasskey(),
|
mutationFn: () => authClient.passkey.addPasskey(),
|
||||||
onSuccess: async ({ data, error }) => {
|
onSuccess: async ({ data, error }) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when passkey registration fails",
|
comment: "Fallback toast when passkey registration fails",
|
||||||
message: "Failed to register passkey. Please try again.",
|
message: "Failed to register passkey. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(t`Passkey registered successfully.`);
|
toast.add({ type: "success", description: t`Passkey registered successfully.` });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
|
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
|
||||||
|
|
||||||
const name = await prompt(t`Enter a name for your passkey.`, {
|
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 });
|
const { error: renameError } = await authClient.passkey.updatePasskey({ id: passkeyId, name: passkeyName });
|
||||||
if (renameError) {
|
if (renameError) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
renameError,
|
renameError,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when renaming a passkey fails",
|
comment: "Fallback toast when renaming a passkey fails",
|
||||||
message: "Failed to rename passkey. Please try again.",
|
message: "Failed to rename passkey. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
|
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
|
||||||
},
|
},
|
||||||
onError: () => {
|
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 }),
|
mutationFn: (id: string) => authClient.passkey.deletePasskey({ id }),
|
||||||
onSuccess: async ({ error }) => {
|
onSuccess: async ({ error }) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when deleting a passkey fails",
|
comment: "Fallback toast when deleting a passkey fails",
|
||||||
message: "Failed to delete passkey. Please try again.",
|
message: "Failed to delete passkey. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(t`Passkey deleted successfully.`);
|
toast.add({ type: "success", description: t`Passkey deleted successfully.` });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
|
await queryClient.invalidateQueries({ queryKey: ["auth", "passkeys"] });
|
||||||
},
|
},
|
||||||
onError: () => {
|
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 { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { i18n } from "@lingui/core";
|
import { i18n } from "@lingui/core";
|
||||||
import { I18nProvider } from "@lingui/react";
|
import { I18nProvider } from "@lingui/react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
|
|
||||||
type MutationName = "create" | "test" | "update" | "delete";
|
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: {} });
|
i18n.loadAndActivate({ locale: "en", messages: {} });
|
||||||
|
|
||||||
@@ -208,11 +208,13 @@ describe("AISettingsSection", () => {
|
|||||||
renderSection();
|
renderSection();
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Test" }));
|
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.
|
// Toast reports only the outcome; the card carries the detail.
|
||||||
expect(toast.error).toHaveBeenCalledWith("Connection failed.");
|
expect(toast.add).toHaveBeenCalledWith({ type: "error", description: "Connection failed." });
|
||||||
expect(toast.error).not.toHaveBeenCalledWith(expect.stringContaining("rejected the API key"));
|
expect(toast.add).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ description: expect.stringContaining("rejected the API key") }),
|
||||||
|
);
|
||||||
|
|
||||||
providers.data = [failed];
|
providers.data = [failed];
|
||||||
renderSection();
|
renderSection();
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { AI_PROVIDER_DEFAULT_BASE_URLS } from "@reactive-resume/ai/types";
|
import { AI_PROVIDER_DEFAULT_BASE_URLS } from "@reactive-resume/ai/types";
|
||||||
import { Badge } from "@reactive-resume/ui/components/badge";
|
import { Badge } from "@reactive-resume/ui/components/badge";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
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 { Label } from "@reactive-resume/ui/components/label";
|
||||||
import { Spinner } from "@reactive-resume/ui/components/spinner";
|
import { Spinner } from "@reactive-resume/ui/components/spinner";
|
||||||
import { Switch } from "@reactive-resume/ui/components/switch";
|
import { Switch } from "@reactive-resume/ui/components/switch";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { cn } from "@reactive-resume/utils/style";
|
import { cn } from "@reactive-resume/utils/style";
|
||||||
import { Combobox } from "@/components/ui/combobox";
|
import { Combobox } from "@/components/ui/combobox";
|
||||||
import { useHasUsableAiProvider } from "@/features/settings/integrations/hooks/use-has-usable-ai-provider";
|
import { useHasUsableAiProvider } from "@/features/settings/integrations/hooks/use-has-usable-ai-provider";
|
||||||
@@ -263,7 +263,11 @@ function ProviderRow({ provider }: ProviderRowProps) {
|
|||||||
setIsEditingModel(false);
|
setIsEditingModel(false);
|
||||||
void invalidate();
|
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(),
|
onSuccess: () => void invalidate(),
|
||||||
onError: (error) =>
|
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) => {
|
onSuccess: (response) => {
|
||||||
if (response.testStatus === "success") {
|
if (response.testStatus === "success") {
|
||||||
toast.success(t`Provider connection verified.`);
|
toast.add({ type: "success", description: t`Provider connection verified.` });
|
||||||
} else {
|
} else {
|
||||||
// The reason persists on the card below, so the toast only reports the outcome.
|
// 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();
|
void invalidate();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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();
|
void invalidate();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -388,7 +398,10 @@ function ProviderRow({ provider }: ProviderRowProps) {
|
|||||||
{
|
{
|
||||||
onSuccess: () => void invalidate(),
|
onSuccess: () => void invalidate(),
|
||||||
onError: (error) =>
|
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 { BookOpenIcon, KeyIcon, LinkSimpleIcon, PlusIcon, TrashSimpleIcon } from "@phosphor-icons/react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AnimatePresence, m } from "motion/react";
|
import { AnimatePresence, m } from "motion/react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { Separator } from "@reactive-resume/ui/components/separator";
|
import { Separator } from "@reactive-resume/ui/components/separator";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { useDialogStore } from "@/dialogs/store";
|
import { useDialogStore } from "@/dialogs/store";
|
||||||
import { useConfirm } from "@/hooks/use-confirm";
|
import { useConfirm } from "@/hooks/use-confirm";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
@@ -43,25 +43,26 @@ export function ApiKeysSettingsPage() {
|
|||||||
|
|
||||||
if (!confirmation) return;
|
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 });
|
const { error } = await authClient.apiKey.delete({ keyId: id });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when deleting an API key fails",
|
comment: "Fallback toast when deleting an API key fails",
|
||||||
message: "Failed to delete the API key. Please try again.",
|
message: "Failed to delete the API key. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
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"] });
|
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 { useStore } from "@tanstack/react-form";
|
||||||
import { useRouteContext, useRouter } from "@tanstack/react-router";
|
import { useRouteContext, useRouter } from "@tanstack/react-router";
|
||||||
import { AnimatePresence, m } from "motion/react";
|
import { AnimatePresence, m } from "motion/react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { getReadableErrorMessage } from "@/libs/error-message";
|
import { getReadableErrorMessage } from "@/libs/error-message";
|
||||||
import { useAppForm } from "@/libs/tanstack-form";
|
import { useAppForm } from "@/libs/tanstack-form";
|
||||||
@@ -51,19 +51,20 @@ export function ProfileSettingsPage({ session }: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when updating profile details fails",
|
comment: "Fallback toast when updating profile details fails",
|
||||||
message: "Failed to update your profile. Please try again.",
|
message: "Failed to update your profile. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
});
|
||||||
return;
|
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 });
|
form.reset({ name: value.name, username: value.username, email: session.user.email });
|
||||||
void router.invalidate();
|
void router.invalidate();
|
||||||
|
|
||||||
@@ -74,21 +75,23 @@ export function ProfileSettingsPage({ session }: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when requesting email change confirmation fails",
|
comment: "Fallback toast when requesting email change confirmation fails",
|
||||||
message: "Failed to request email change. Please try again.",
|
message: "Failed to request email change. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(
|
toast.add({
|
||||||
t`A confirmation link has been sent to your current email address. Please check your inbox to confirm the change.`,
|
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 });
|
form.reset({ name: value.name, username: value.username, email: session.user.email });
|
||||||
void router.invalidate();
|
void router.invalidate();
|
||||||
}
|
}
|
||||||
@@ -102,7 +105,7 @@ export function ProfileSettingsPage({ session }: Props) {
|
|||||||
const isDirty = useStore(form.store, (s) => s.isDirty);
|
const isDirty = useStore(form.store, (s) => s.isDirty);
|
||||||
|
|
||||||
const handleResendVerificationEmail = async () => {
|
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({
|
const { error } = await authClient.sendVerificationEmail({
|
||||||
email: session.user.email,
|
email: session.user.email,
|
||||||
@@ -110,23 +113,25 @@ export function ProfileSettingsPage({ session }: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when resending account verification email fails",
|
comment: "Fallback toast when resending account verification email fails",
|
||||||
message: "Failed to resend verification email. Please try again.",
|
message: "Failed to resend verification email. Please try again.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ id: toastId },
|
id: toastId,
|
||||||
);
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(
|
toast.add({
|
||||||
t`A new verification link has been sent to your email address. Please check your inbox to verify your account.`,
|
type: "success",
|
||||||
{ id: toastId },
|
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();
|
void router.invalidate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useLingui } from "@lingui/react";
|
|||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { PaletteIcon, SignOutIcon, TranslateIcon } from "@phosphor-icons/react";
|
import { PaletteIcon, SignOutIcon, TranslateIcon } from "@phosphor-icons/react";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useIsClient } from "usehooks-ts";
|
import { useIsClient } from "usehooks-ts";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -19,6 +18,7 @@ import {
|
|||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@reactive-resume/ui/components/dropdown-menu";
|
} from "@reactive-resume/ui/components/dropdown-menu";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { useTheme } from "@/features/theme/provider";
|
import { useTheme } from "@/features/theme/provider";
|
||||||
import { authClient } from "@/libs/auth/client";
|
import { authClient } from "@/libs/auth/client";
|
||||||
import { getReadableErrorMessage } from "@/libs/error-message";
|
import { getReadableErrorMessage } from "@/libs/error-message";
|
||||||
@@ -42,25 +42,26 @@ export function UserDropdownMenu({ children }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
const toastId = toast.loading(t`Signing out...`);
|
const toastId = toast.add({ type: "loading", description: t`Signing out...` });
|
||||||
|
|
||||||
await authClient.signOut({
|
await authClient.signOut({
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
void router.invalidate();
|
void router.invalidate();
|
||||||
},
|
},
|
||||||
onError: ({ error }) => {
|
onError: ({ error }) => {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when signing out fails",
|
comment: "Fallback toast when signing out fails",
|
||||||
message: "Failed to sign out. Please try again.",
|
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 { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||||
import { domAnimation, LazyMotion, MotionConfig } from "motion/react";
|
import { domAnimation, LazyMotion, MotionConfig } from "motion/react";
|
||||||
import { useEffect } from "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 { TooltipProvider } from "@reactive-resume/ui/components/tooltip";
|
||||||
import { BreakpointIndicator } from "@/components/layout/breakpoint-indicator";
|
import { BreakpointIndicator } from "@/components/layout/breakpoint-indicator";
|
||||||
import { DonationToast } from "@/components/ui/donation-toast";
|
import { DonationToast } from "@/components/ui/donation-toast";
|
||||||
@@ -136,7 +136,7 @@ function RootComponent() {
|
|||||||
{!isBuilder && <DonationToast />}
|
{!isBuilder && <DonationToast />}
|
||||||
<DialogManager />
|
<DialogManager />
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<Toaster richColors position="bottom-center" />
|
<Toaster />
|
||||||
|
|
||||||
{import.meta.env.DEV && <BreakpointIndicator />}
|
{import.meta.env.DEV && <BreakpointIndicator />}
|
||||||
{import.meta.env.DEV && (
|
{import.meta.env.DEV && (
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { ArrowRightIcon, ChatCircleDotsIcon, FilePlusIcon, GearSixIcon } from "@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { Link, useNavigate } from "@tanstack/react-router";
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useIsClient } from "usehooks-ts";
|
import { useIsClient } from "usehooks-ts";
|
||||||
import { Badge } from "@reactive-resume/ui/components/badge";
|
import { Badge } from "@reactive-resume/ui/components/badge";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { Label } from "@reactive-resume/ui/components/label";
|
import { Label } from "@reactive-resume/ui/components/label";
|
||||||
import { Spinner } from "@reactive-resume/ui/components/spinner";
|
import { Spinner } from "@reactive-resume/ui/components/spinner";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { Combobox } from "@/components/ui/combobox";
|
import { Combobox } from "@/components/ui/combobox";
|
||||||
import { useHasUsableAiProvider } from "@/features/settings/integrations/hooks/use-has-usable-ai-provider";
|
import { useHasUsableAiProvider } from "@/features/settings/integrations/hooks/use-has-usable-ai-provider";
|
||||||
import { getOrpcErrorMessage } from "@/libs/error-message";
|
import { getOrpcErrorMessage } from "@/libs/error-message";
|
||||||
@@ -176,15 +176,16 @@ export function NewThreadSetup({ resumeId }: NewThreadSetupProps) {
|
|||||||
void navigate({ to: "/agent/$threadId", params: { threadId: thread.id } });
|
void navigate({ to: "/agent/$threadId", params: { threadId: thread.id } });
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) =>
|
||||||
toast.error(
|
toast.add({
|
||||||
getOrpcErrorMessage(error, {
|
type: "error",
|
||||||
|
description: getOrpcErrorMessage(error, {
|
||||||
byCode: {
|
byCode: {
|
||||||
PRECONDITION_FAILED: t`AI agent setup is unavailable until REDIS_URL and ENCRYPTION_SECRET are configured.`,
|
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.`,
|
BAD_REQUEST: t`Set up an AI provider before starting a thread.`,
|
||||||
},
|
},
|
||||||
fallback: t`Failed to start agent 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 { ArrowSquareOutIcon, CircleNotchIcon, FilePdfIcon, MinusIcon, PlusIcon } from "@phosphor-icons/react";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
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 { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/components/tooltip";
|
||||||
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
|
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
|
||||||
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
|
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
|
||||||
@@ -74,7 +74,10 @@ export function ResumePane({ resume }: ResumePaneProps) {
|
|||||||
if (!resume) return;
|
if (!resume) return;
|
||||||
|
|
||||||
const filename = generateFilename(resume.name || resume.data.basics.name || resume.id, "pdf");
|
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);
|
setIsPrinting(true);
|
||||||
|
|
||||||
@@ -82,10 +85,10 @@ export function ResumePane({ resume }: ResumePaneProps) {
|
|||||||
const blob = await createResumePdfBlob(resume.data);
|
const blob = await createResumePdfBlob(resume.data);
|
||||||
downloadWithAnchor(blob, filename);
|
downloadWithAnchor(blob, filename);
|
||||||
} catch {
|
} 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 {
|
} finally {
|
||||||
setIsPrinting(false);
|
setIsPrinting(false);
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
}
|
}
|
||||||
}, [resume]);
|
}, [resume]);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Link, useNavigate } from "@tanstack/react-router";
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -22,6 +21,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@reactive-resume/ui/components/dropdown-menu";
|
} from "@reactive-resume/ui/components/dropdown-menu";
|
||||||
import { ScrollArea } from "@reactive-resume/ui/components/scroll-area";
|
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 { cn } from "@reactive-resume/utils/style";
|
||||||
import { useConfirm } from "@/hooks/use-confirm";
|
import { useConfirm } from "@/hooks/use-confirm";
|
||||||
import { getOrpcErrorMessage } from "@/libs/error-message";
|
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() });
|
await queryClient.invalidateQueries({ queryKey: orpc.agent.threads.list.queryKey() });
|
||||||
if (activeThreadId === thread.id) void navigate({ to: "/agent" });
|
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 { useNavigate } from "@tanstack/react-router";
|
||||||
import { m } from "motion/react";
|
import { m } from "motion/react";
|
||||||
import { useControls, useTransformComponent } from "react-zoom-pan-pinch";
|
import { useControls, useTransformComponent } from "react-zoom-pan-pinch";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useCopyToClipboard } from "usehooks-ts";
|
import { useCopyToClipboard } from "usehooks-ts";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +24,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@reactive-resume/ui/components/dropdown-menu";
|
} 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 { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/components/tooltip";
|
||||||
import { cn } from "@reactive-resume/utils/style";
|
import { cn } from "@reactive-resume/utils/style";
|
||||||
import {
|
import {
|
||||||
@@ -108,7 +108,7 @@ export function BuilderDock({ pageLayout, onTogglePageLayout }: BuilderDockProps
|
|||||||
title={t`Copy URL`}
|
title={t`Copy URL`}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await copyToClipboard(publicUrl);
|
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>
|
</m.div>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Link, useNavigate } from "@tanstack/react-router";
|
import { Link, useNavigate } from "@tanstack/react-router";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { match } from "ts-pattern";
|
import { match } from "ts-pattern";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +25,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@reactive-resume/ui/components/dropdown-menu";
|
} from "@reactive-resume/ui/components/dropdown-menu";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { useDialogStore } from "@/dialogs/store";
|
import { useDialogStore } from "@/dialogs/store";
|
||||||
import {
|
import {
|
||||||
useCurrentBuilderResumeSelector,
|
useCurrentBuilderResumeSelector,
|
||||||
@@ -205,7 +205,7 @@ function BuilderHeaderDropdown() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(getResumeErrorMessage(error));
|
toast.add({ type: "error", description: getResumeErrorMessage(error) });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -218,17 +218,17 @@ function BuilderHeaderDropdown() {
|
|||||||
|
|
||||||
if (!confirmation) return;
|
if (!confirmation) return;
|
||||||
|
|
||||||
const toastId = toast.loading(t`Deleting your resume...`);
|
const toastId = toast.add({ type: "loading", description: t`Deleting your resume...` });
|
||||||
|
|
||||||
deleteResume(
|
deleteResume(
|
||||||
{ id },
|
{ id },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
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: [] } });
|
void navigate({ to: "/dashboard/resumes", search: { sort: "lastUpdatedAt", tags: [] } });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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 { t } from "@lingui/core/macro";
|
||||||
import { FloppyDiskIcon } from "@phosphor-icons/react";
|
|
||||||
import { useHotkey } from "@tanstack/react-hotkeys";
|
import { useHotkey } from "@tanstack/react-hotkeys";
|
||||||
import { Suspense, useState } from "react";
|
import { Suspense, useState } from "react";
|
||||||
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
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 { LoadingScreen } from "@/components/layout/loading-screen";
|
||||||
import { ResumePreview } from "@/features/resume/preview/preview";
|
import { ResumePreview } from "@/features/resume/preview/preview";
|
||||||
import { BuilderDock } from "./dock";
|
import { BuilderDock } from "./dock";
|
||||||
@@ -14,7 +13,11 @@ export function PreviewPage() {
|
|||||||
const [pageLayout, setPageLayout] = useState(DEFAULT_BUILDER_PREVIEW_PAGE_LAYOUT);
|
const [pageLayout, setPageLayout] = useState(DEFAULT_BUILDER_PREVIEW_PAGE_LAYOUT);
|
||||||
|
|
||||||
useHotkey("Mod+S", () => {
|
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 (
|
return (
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Trans } from "@lingui/react/macro";
|
|||||||
import { ClockCounterClockwiseIcon } from "@phosphor-icons/react";
|
import { ClockCounterClockwiseIcon } from "@phosphor-icons/react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@reactive-resume/ui/components/dropdown-menu";
|
} from "@reactive-resume/ui/components/dropdown-menu";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { useResumeStore } from "@/features/resume/builder/draft";
|
import { useResumeStore } from "@/features/resume/builder/draft";
|
||||||
import { useConfirm } from "@/hooks/use-confirm";
|
import { useConfirm } from "@/hooks/use-confirm";
|
||||||
import { getResumeErrorMessage } from "@/libs/error-message";
|
import { getResumeErrorMessage } from "@/libs/error-message";
|
||||||
@@ -53,9 +53,9 @@ export function BuilderVersionHistory({ resumeId }: BuilderVersionHistoryProps)
|
|||||||
replaceResumeFromServer(restored as Resume);
|
replaceResumeFromServer(restored as Resume);
|
||||||
queryClient.setQueryData(orpc.resume.getById.queryOptions({ input: { id: resumeId } }).queryKey, restored);
|
queryClient.setQueryData(orpc.resume.getById.queryOptions({ input: { id: resumeId } }).queryKey, restored);
|
||||||
void queryClient.invalidateQueries({ queryKey: orpc.resume.listVersions.queryKey({ input: { resumeId } }) });
|
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) {
|
} 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 { LockSimpleIcon } from "@phosphor-icons/react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Fragment, useCallback, useRef } from "react";
|
import { Fragment, useCallback, useRef } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { match } from "ts-pattern";
|
import { match } from "ts-pattern";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@reactive-resume/ui/components/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@reactive-resume/ui/components/avatar";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { ScrollArea } from "@reactive-resume/ui/components/scroll-area";
|
import { ScrollArea } from "@reactive-resume/ui/components/scroll-area";
|
||||||
import { Separator } from "@reactive-resume/ui/components/separator";
|
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 { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/components/tooltip";
|
||||||
import { getInitials } from "@reactive-resume/utils/string";
|
import { getInitials } from "@reactive-resume/utils/string";
|
||||||
import { useCurrentResume, useIsResumeLocked, usePatchResume } from "@/features/resume/builder/draft";
|
import { useCurrentResume, useIsResumeLocked, usePatchResume } from "@/features/resume/builder/draft";
|
||||||
@@ -98,7 +98,7 @@ function LockBanner() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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 { useMutation } from "@tanstack/react-query";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import Cropper from "react-easy-crop";
|
import Cropper from "react-easy-crop";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { pictureSchema } from "@reactive-resume/schema/resume/data";
|
import { pictureSchema } from "@reactive-resume/schema/resume/data";
|
||||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
InputGroupText,
|
InputGroupText,
|
||||||
} from "@reactive-resume/ui/components/input-group";
|
} from "@reactive-resume/ui/components/input-group";
|
||||||
import { Slider } from "@reactive-resume/ui/components/slider";
|
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 "react-easy-crop/react-easy-crop.css";
|
||||||
import { ColorPicker } from "@/components/input/color-picker";
|
import { ColorPicker } from "@/components/input/color-picker";
|
||||||
import { useCurrentBuilderResumeSelector, useUpdateResumeData } from "@/features/resume/builder/draft";
|
import { useCurrentBuilderResumeSelector, useUpdateResumeData } from "@/features/resume/builder/draft";
|
||||||
@@ -490,26 +490,27 @@ function PictureSectionForm() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const uploadPictureFile = (file: File) => {
|
const uploadPictureFile = (file: File) => {
|
||||||
const toastId = toast.loading(t`Uploading picture…`);
|
const toastId = toast.add({ type: "loading", description: t`Uploading picture…` });
|
||||||
|
|
||||||
uploadFile(file, {
|
uploadFile(file, {
|
||||||
onSuccess: ({ url }) => {
|
onSuccess: ({ url }) => {
|
||||||
form.setFieldValue("url", url);
|
form.setFieldValue("url", url);
|
||||||
handleAutoSave();
|
handleAutoSave();
|
||||||
toast.dismiss(toastId);
|
toast.close(toastId);
|
||||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(
|
toast.add({
|
||||||
getReadableErrorMessage(
|
type: "error",
|
||||||
|
description: getReadableErrorMessage(
|
||||||
error,
|
error,
|
||||||
t({
|
t({
|
||||||
comment: "Fallback toast when uploading profile picture for resume fails",
|
comment: "Fallback toast when uploading profile picture for resume fails",
|
||||||
message: "Failed to upload picture. Please try again.",
|
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 { ArrowRightIcon, InfoIcon, LightningIcon, SparkleIcon } from "@phosphor-icons/react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { match } from "ts-pattern";
|
import { match } from "ts-pattern";
|
||||||
import { Alert, AlertDescription } from "@reactive-resume/ui/components/alert";
|
import { Alert, AlertDescription } from "@reactive-resume/ui/components/alert";
|
||||||
import { Badge } from "@reactive-resume/ui/components/badge";
|
import { Badge } from "@reactive-resume/ui/components/badge";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { useResume } from "@/features/resume/builder/draft";
|
import { useResume } from "@/features/resume/builder/draft";
|
||||||
import { getOrpcErrorMessage } from "@/libs/error-message";
|
|
||||||
import { orpc } from "@/libs/orpc/client";
|
import { orpc } from "@/libs/orpc/client";
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@@ -62,27 +61,10 @@ export function ResumeAnalysisSectionBuilder() {
|
|||||||
...orpc.ai.analyzeResume.mutationOptions(),
|
...orpc.ai.analyzeResume.mutationOptions(),
|
||||||
onSuccess: (analysis) => {
|
onSuccess: (analysis) => {
|
||||||
queryClient.setQueryData(orpc.resume.analysis.getById.queryKey({ input: { id: resumeId } }), 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) => {
|
onError: (_error) => {
|
||||||
toast.error(t`Failed to analyze resume.`, {
|
toast.add({ type: "error", description: 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.",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { ORPCError } from "@orpc/client";
|
|||||||
import { ClipboardIcon, LockSimpleIcon, LockSimpleOpenIcon } from "@phosphor-icons/react";
|
import { ClipboardIcon, LockSimpleIcon, LockSimpleOpenIcon } from "@phosphor-icons/react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useCopyToClipboard } from "usehooks-ts";
|
import { useCopyToClipboard } from "usehooks-ts";
|
||||||
import { Button } from "@reactive-resume/ui/components/button";
|
import { Button } from "@reactive-resume/ui/components/button";
|
||||||
import { Input } from "@reactive-resume/ui/components/input";
|
import { Input } from "@reactive-resume/ui/components/input";
|
||||||
import { Label } from "@reactive-resume/ui/components/label";
|
import { Label } from "@reactive-resume/ui/components/label";
|
||||||
import { Switch } from "@reactive-resume/ui/components/switch";
|
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 { useCurrentResume, usePatchResume } from "@/features/resume/builder/draft";
|
||||||
import { useConfirm } from "@/hooks/use-confirm";
|
import { useConfirm } from "@/hooks/use-confirm";
|
||||||
import { usePrompt } from "@/hooks/use-prompt";
|
import { usePrompt } from "@/hooks/use-prompt";
|
||||||
@@ -33,7 +33,7 @@ export function SharingSectionBuilder() {
|
|||||||
|
|
||||||
const onCopyUrl = useCallback(async () => {
|
const onCopyUrl = useCallback(async () => {
|
||||||
await copyToClipboard(publicUrl);
|
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]);
|
}, [publicUrl, copyToClipboard]);
|
||||||
|
|
||||||
const onTogglePublic = useCallback(
|
const onTogglePublic = useCallback(
|
||||||
@@ -45,7 +45,7 @@ export function SharingSectionBuilder() {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof ORPCError ? error.message : t`Something went wrong. Please try again.`;
|
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],
|
[patchResume, resume.id, updateResume],
|
||||||
@@ -64,19 +64,19 @@ export function SharingSectionBuilder() {
|
|||||||
if (!value) return;
|
if (!value) return;
|
||||||
|
|
||||||
const password = value.trim();
|
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 {
|
try {
|
||||||
await setPassword({ id: resume.id, password });
|
await setPassword({ id: resume.id, password });
|
||||||
patchResume((draft) => {
|
patchResume((draft) => {
|
||||||
draft.hasPassword = true;
|
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) {
|
} catch (error) {
|
||||||
const message = error instanceof ORPCError ? error.message : t`Something went wrong. Please try again.`;
|
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]);
|
}, [patchResume, prompt, resume.id, setPassword]);
|
||||||
|
|
||||||
@@ -90,17 +90,17 @@ export function SharingSectionBuilder() {
|
|||||||
});
|
});
|
||||||
if (!confirmation) return;
|
if (!confirmation) return;
|
||||||
|
|
||||||
const toastId = toast.loading(t`Removing password protection...`);
|
const toastId = toast.add({ type: "loading", description: t`Removing password protection...` });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await removePassword({ id: resume.id });
|
await removePassword({ id: resume.id });
|
||||||
patchResume((draft) => {
|
patchResume((draft) => {
|
||||||
draft.hasPassword = false;
|
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) {
|
} catch (error) {
|
||||||
const message = error instanceof ORPCError ? error.message : t`Something went wrong. Please try again.`;
|
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]);
|
}, [confirm, patchResume, removePassword, resume.hasPassword, resume.id]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { RouterOutput } from "@/libs/orpc/client";
|
import type { RouterOutput } from "@/libs/orpc/client";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "@reactive-resume/ui/components/toast";
|
||||||
import { useDialogStore } from "@/dialogs/store";
|
import { useDialogStore } from "@/dialogs/store";
|
||||||
import { useConfirm } from "@/hooks/use-confirm";
|
import { useConfirm } from "@/hooks/use-confirm";
|
||||||
import { getResumeErrorMessage } from "@/libs/error-message";
|
import { getResumeErrorMessage } from "@/libs/error-message";
|
||||||
@@ -25,7 +25,7 @@ export function useResumeMenuActions(resume: Resume) {
|
|||||||
|
|
||||||
setLockedResume(
|
setLockedResume(
|
||||||
{ id: resume.id, isLocked: !resume.isLocked },
|
{ 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;
|
if (!confirmed) return;
|
||||||
|
|
||||||
const toastId = toast.loading(t`Deleting your resume...`);
|
const toastId = toast.add({ type: "loading", description: t`Deleting your resume...` });
|
||||||
deleteResume(
|
deleteResume(
|
||||||
{ id: resume.id },
|
{ id: resume.id },
|
||||||
{
|
{
|
||||||
onSuccess: () => toast.success(t`Your resume has been deleted successfully.`, { id: toastId }),
|
onSuccess: () =>
|
||||||
onError: (error) => toast.error(getResumeErrorMessage(error), { id: toastId }),
|
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