mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-06 09:59:00 +02:00
♻️ Replace old toaster with sonner (#1801)
This commit is contained in:
parent
ac75e690a8
commit
961a493a29
11 changed files with 87 additions and 312 deletions
|
@ -12,7 +12,6 @@ import {
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@rallly/ui/form";
|
} from "@rallly/ui/form";
|
||||||
import { useToast } from "@rallly/ui/hooks/use-toast";
|
|
||||||
import { Input } from "@rallly/ui/input";
|
import { Input } from "@rallly/ui/input";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { InfoIcon } from "lucide-react";
|
import { InfoIcon } from "lucide-react";
|
||||||
|
@ -24,6 +23,7 @@ import { z } from "zod";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
import { trpc } from "@/trpc/client";
|
import { trpc } from "@/trpc/client";
|
||||||
|
import { toast } from "@rallly/ui/sonner";
|
||||||
|
|
||||||
const emailChangeFormData = z.object({
|
const emailChangeFormData = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
|
@ -41,7 +41,6 @@ export const ProfileEmailAddress = () => {
|
||||||
resolver: zodResolver(emailChangeFormData),
|
resolver: zodResolver(emailChangeFormData),
|
||||||
});
|
});
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const [didRequestEmailChange, setDidRequestEmailChange] =
|
const [didRequestEmailChange, setDidRequestEmailChange] =
|
||||||
React.useState(false);
|
React.useState(false);
|
||||||
|
@ -52,35 +51,38 @@ export const ProfileEmailAddress = () => {
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
posthog.capture("email change completed");
|
posthog.capture("email change completed");
|
||||||
toast({
|
toast.message(
|
||||||
title: t("emailChangeSuccess", {
|
t("emailChangeSuccess", {
|
||||||
defaultValue: "Email changed successfully",
|
defaultValue: "Email changed successfully",
|
||||||
}),
|
}),
|
||||||
description: t("emailChangeSuccessDescription", {
|
{
|
||||||
defaultValue: "Your email has been updated",
|
description: t("emailChangeSuccessDescription", {
|
||||||
}),
|
defaultValue: "Your email has been updated",
|
||||||
});
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
posthog.capture("email change failed", { error });
|
posthog.capture("email change failed", { error });
|
||||||
toast({
|
toast.error(
|
||||||
variant: "destructive",
|
t("emailChangeFailed", {
|
||||||
title: t("emailChangeFailed", {
|
|
||||||
defaultValue: "Email change failed",
|
defaultValue: "Email change failed",
|
||||||
}),
|
}),
|
||||||
description:
|
{
|
||||||
error === "invalidToken"
|
description:
|
||||||
? t("emailChangeInvalidToken", {
|
error === "invalidToken"
|
||||||
defaultValue:
|
? t("emailChangeInvalidToken", {
|
||||||
"The verification link is invalid or has expired. Please try again.",
|
defaultValue:
|
||||||
})
|
"The verification link is invalid or has expired. Please try again.",
|
||||||
: t("emailChangeError", {
|
})
|
||||||
defaultValue: "An error occurred while changing your email",
|
: t("emailChangeError", {
|
||||||
}),
|
defaultValue: "An error occurred while changing your email",
|
||||||
});
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [posthog, t, toast]);
|
}, [posthog, t]);
|
||||||
|
|
||||||
const { handleSubmit, formState, reset } = form;
|
const { handleSubmit, formState, reset } = form;
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { usePostHog } from "@rallly/posthog/client";
|
import { usePostHog } from "@rallly/posthog/client";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { useToast } from "@rallly/ui/hooks/use-toast";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
@ -12,6 +11,7 @@ import { useUser } from "@/components/user-provider";
|
||||||
import { useFeatureFlag } from "@/features/feature-flags/client";
|
import { useFeatureFlag } from "@/features/feature-flags/client";
|
||||||
import { useTranslation } from "@/i18n/client";
|
import { useTranslation } from "@/i18n/client";
|
||||||
import { trpc } from "@/trpc/client";
|
import { trpc } from "@/trpc/client";
|
||||||
|
import { toast } from "@rallly/ui/sonner";
|
||||||
|
|
||||||
const allowedMimeTypes = z.enum(["image/jpeg", "image/png"]);
|
const allowedMimeTypes = z.enum(["image/jpeg", "image/png"]);
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ function ChangeAvatarButton({ onSuccess }: { onSuccess: () => void }) {
|
||||||
const getPresignedUrl = trpc.user.getAvatarUploadUrl.useMutation();
|
const getPresignedUrl = trpc.user.getAvatarUploadUrl.useMutation();
|
||||||
const updateAvatar = trpc.user.updateAvatar.useMutation();
|
const updateAvatar = trpc.user.updateAvatar.useMutation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
const handleFileChange = async (
|
const handleFileChange = async (
|
||||||
|
@ -32,28 +31,32 @@ function ChangeAvatarButton({ onSuccess }: { onSuccess: () => void }) {
|
||||||
const parsedFileType = allowedMimeTypes.safeParse(file.type);
|
const parsedFileType = allowedMimeTypes.safeParse(file.type);
|
||||||
|
|
||||||
if (!parsedFileType.success) {
|
if (!parsedFileType.success) {
|
||||||
toast({
|
toast.message(
|
||||||
title: t("invalidFileType", {
|
t("invalidFileType", {
|
||||||
defaultValue: "Invalid file type",
|
defaultValue: "Invalid file type",
|
||||||
}),
|
}),
|
||||||
description: t("invalidFileTypeDescription", {
|
{
|
||||||
defaultValue: "Please upload a JPG or PNG file.",
|
description: t("invalidFileTypeDescription", {
|
||||||
}),
|
defaultValue: "Please upload a JPG or PNG file.",
|
||||||
});
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileType = parsedFileType.data;
|
const fileType = parsedFileType.data;
|
||||||
|
|
||||||
if (file.size > 2 * 1024 * 1024) {
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
toast({
|
toast.message(
|
||||||
title: t("fileTooLarge", {
|
t("fileTooLarge", {
|
||||||
defaultValue: "File too large",
|
defaultValue: "File too large",
|
||||||
}),
|
}),
|
||||||
description: t("fileTooLargeDescription", {
|
{
|
||||||
defaultValue: "Please upload a file smaller than 2MB.",
|
description: t("fileTooLargeDescription", {
|
||||||
}),
|
defaultValue: "Please upload a file smaller than 2MB.",
|
||||||
});
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
@ -81,15 +84,17 @@ function ChangeAvatarButton({ onSuccess }: { onSuccess: () => void }) {
|
||||||
|
|
||||||
onSuccess();
|
onSuccess();
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast.error(
|
||||||
title: t("errorUploadPicture", {
|
t("errorUploadPicture", {
|
||||||
defaultValue: "Failed to upload",
|
defaultValue: "Failed to upload",
|
||||||
}),
|
}),
|
||||||
description: t("errorUploadPictureDescription", {
|
{
|
||||||
defaultValue:
|
description: t("errorUploadPictureDescription", {
|
||||||
"There was an issue uploading your picture. Please try again later.",
|
defaultValue:
|
||||||
}),
|
"There was an issue uploading your picture. Please try again later.",
|
||||||
});
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ import {
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
} from "@rallly/ui/form";
|
} from "@rallly/ui/form";
|
||||||
import { useToast } from "@rallly/ui/hooks/use-toast";
|
import { toast } from "@rallly/ui/sonner";
|
||||||
import { Switch } from "@rallly/ui/switch";
|
import { Switch } from "@rallly/ui/switch";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
@ -45,8 +45,6 @@ export function InstanceSettingsForm({
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
@ -57,15 +55,17 @@ export function InstanceSettingsForm({
|
||||||
form.reset(data);
|
form.reset(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toast({
|
toast.error(
|
||||||
title: t("unexpectedError", {
|
t("unexpectedError", {
|
||||||
defaultValue: "Unexpected Error",
|
defaultValue: "Unexpected Error",
|
||||||
}),
|
}),
|
||||||
description: t("unexpectedErrorDescription", {
|
{
|
||||||
defaultValue:
|
description: t("unexpectedErrorDescription", {
|
||||||
"There was an unexpected error. Please try again later.",
|
defaultValue:
|
||||||
}),
|
"There was an unexpected error. Please try again later.",
|
||||||
});
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
|
@ -2,8 +2,7 @@ import "../../style.css";
|
||||||
|
|
||||||
import { supportedLngs } from "@rallly/languages";
|
import { supportedLngs } from "@rallly/languages";
|
||||||
import { PostHogProvider, posthog } from "@rallly/posthog/client";
|
import { PostHogProvider, posthog } from "@rallly/posthog/client";
|
||||||
import { Toaster as SonnerToast } from "@rallly/ui/sonner";
|
import { Toaster } from "@rallly/ui/sonner";
|
||||||
import { Toaster } from "@rallly/ui/toaster";
|
|
||||||
import { TooltipProvider } from "@rallly/ui/tooltip";
|
import { TooltipProvider } from "@rallly/ui/tooltip";
|
||||||
import { LazyMotion, domAnimation } from "motion/react";
|
import { LazyMotion, domAnimation } from "motion/react";
|
||||||
import type { Viewport } from "next";
|
import type { Viewport } from "next";
|
||||||
|
@ -72,7 +71,6 @@ export default async function Root({
|
||||||
<body>
|
<body>
|
||||||
<FeatureFlagsProvider value={{ storage: isStorageEnabled }}>
|
<FeatureFlagsProvider value={{ storage: isStorageEnabled }}>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<SonnerToast />
|
|
||||||
<I18nProvider locale={locale}>
|
<I18nProvider locale={locale}>
|
||||||
<TRPCProvider>
|
<TRPCProvider>
|
||||||
<LazyMotion features={domAnimation}>
|
<LazyMotion features={domAnimation}>
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useToast } from "@rallly/ui/hooks/use-toast";
|
import { toast } from "@rallly/ui/sonner";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function UnsubscribeAlert() {
|
export function UnsubscribeAlert() {
|
||||||
const { toast } = useToast();
|
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
|
|
||||||
const urlId = useParams<{ urlId: string }>()?.urlId;
|
const urlId = useParams<{ urlId: string }>()?.urlId;
|
||||||
|
@ -18,17 +17,19 @@ export function UnsubscribeAlert() {
|
||||||
const unsubscribed = Cookies.get(cookieName);
|
const unsubscribed = Cookies.get(cookieName);
|
||||||
if (unsubscribed) {
|
if (unsubscribed) {
|
||||||
Cookies.remove(cookieName);
|
Cookies.remove(cookieName);
|
||||||
toast({
|
toast.message(
|
||||||
title: t("unsubscribeToastTitle", {
|
t("unsubscribeToastTitle", {
|
||||||
defaultValue: "You have disabled notifications",
|
defaultValue: "You have disabled notifications",
|
||||||
}),
|
}),
|
||||||
description: t("unsubscribeToastDescription", {
|
{
|
||||||
defaultValue:
|
description: t("unsubscribeToastDescription", {
|
||||||
"You will no longer receive notifications for this poll",
|
defaultValue:
|
||||||
}),
|
"You will no longer receive notifications for this poll",
|
||||||
});
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [t, toast, urlId]);
|
}, [t, urlId]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@rallly/ui/card";
|
} from "@rallly/ui/card";
|
||||||
import { Form } from "@rallly/ui/form";
|
import { Form } from "@rallly/ui/form";
|
||||||
import { useToast } from "@rallly/ui/hooks/use-toast";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
@ -21,6 +20,7 @@ import { Trans } from "@/components/trans";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
import { trpc } from "@/trpc/client";
|
import { trpc } from "@/trpc/client";
|
||||||
|
|
||||||
|
import { toast } from "@rallly/ui/sonner";
|
||||||
import type { NewEventData } from "./forms";
|
import type { NewEventData } from "./forms";
|
||||||
import { PollDetailsForm, PollOptionsForm } from "./forms";
|
import { PollDetailsForm, PollOptionsForm } from "./forms";
|
||||||
|
|
||||||
|
@ -42,7 +42,6 @@ export interface CreatePollPageProps {
|
||||||
export const CreatePoll: React.FunctionComponent = () => {
|
export const CreatePoll: React.FunctionComponent = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user, createGuestIfNeeded } = useUser();
|
const { user, createGuestIfNeeded } = useUser();
|
||||||
const { toast } = useToast();
|
|
||||||
const form = useForm<NewEventData>({
|
const form = useForm<NewEventData>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: "",
|
title: "",
|
||||||
|
@ -69,10 +68,7 @@ export const CreatePoll: React.FunctionComponent = () => {
|
||||||
networkMode: "always",
|
networkMode: "always",
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (error.data?.code === "BAD_REQUEST") {
|
if (error.data?.code === "BAD_REQUEST") {
|
||||||
toast({
|
toast.error(error.message);
|
||||||
title: "Error",
|
|
||||||
description: error.message,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@rallly/ui/dropdown-menu";
|
} from "@rallly/ui/dropdown-menu";
|
||||||
import { useToast } from "@rallly/ui/hooks/use-toast";
|
|
||||||
import { Icon } from "@rallly/ui/icon";
|
import { Icon } from "@rallly/ui/icon";
|
||||||
import { Input } from "@rallly/ui/input";
|
import { Input } from "@rallly/ui/input";
|
||||||
import { Textarea } from "@rallly/ui/textarea";
|
import { Textarea } from "@rallly/ui/textarea";
|
||||||
|
@ -38,6 +37,7 @@ import { useRole } from "@/contexts/role";
|
||||||
import { useTranslation } from "@/i18n/client";
|
import { useTranslation } from "@/i18n/client";
|
||||||
import { trpc } from "@/trpc/client";
|
import { trpc } from "@/trpc/client";
|
||||||
|
|
||||||
|
import { toast } from "@rallly/ui/sonner";
|
||||||
import { requiredString } from "../../utils/form-validation";
|
import { requiredString } from "../../utils/form-validation";
|
||||||
import TruncatedLinkify from "../poll/truncated-linkify";
|
import TruncatedLinkify from "../poll/truncated-linkify";
|
||||||
import { useUser } from "../user-provider";
|
import { useUser } from "../user-provider";
|
||||||
|
@ -79,17 +79,13 @@ function NewCommentForm({
|
||||||
content: "",
|
content: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const addComment = trpc.polls.comments.add.useMutation({
|
const addComment = trpc.polls.comments.add.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
posthog?.capture("created comment");
|
posthog?.capture("created comment");
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast({
|
toast.error(error.message);
|
||||||
title: "Error",
|
|
||||||
description: error.message,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { usePostHog } from "@rallly/posthog/client";
|
import { usePostHog } from "@rallly/posthog/client";
|
||||||
import { useToast } from "@rallly/ui/hooks/use-toast";
|
|
||||||
|
|
||||||
import { usePoll } from "@/components/poll-context";
|
import { usePoll } from "@/components/poll-context";
|
||||||
import { trpc } from "@/trpc/client";
|
import { trpc } from "@/trpc/client";
|
||||||
|
|
||||||
|
import { toast } from "@rallly/ui/sonner";
|
||||||
import type { ParticipantForm } from "./types";
|
import type { ParticipantForm } from "./types";
|
||||||
|
|
||||||
export const normalizeVotes = (
|
export const normalizeVotes = (
|
||||||
|
@ -81,7 +81,6 @@ export const useDeleteParticipantMutation = () => {
|
||||||
|
|
||||||
export const useUpdatePollMutation = () => {
|
export const useUpdatePollMutation = () => {
|
||||||
const posthog = usePostHog();
|
const posthog = usePostHog();
|
||||||
const { toast } = useToast();
|
|
||||||
return trpc.polls.update.useMutation({
|
return trpc.polls.update.useMutation({
|
||||||
onSuccess: (_data, { urlId }) => {
|
onSuccess: (_data, { urlId }) => {
|
||||||
posthog?.capture("updated poll", {
|
posthog?.capture("updated poll", {
|
||||||
|
@ -90,10 +89,7 @@ export const useUpdatePollMutation = () => {
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (error.data?.code === "BAD_REQUEST") {
|
if (error.data?.code === "BAD_REQUEST") {
|
||||||
toast({
|
toast.error(error.message);
|
||||||
title: "Error",
|
|
||||||
description: error.message,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { usePostHog } from "@rallly/posthog/client";
|
import { usePostHog } from "@rallly/posthog/client";
|
||||||
import { useToast } from "@rallly/ui/hooks/use-toast";
|
|
||||||
import {
|
import {
|
||||||
MutationCache,
|
MutationCache,
|
||||||
QueryClient,
|
QueryClient,
|
||||||
|
@ -13,11 +12,11 @@ import superjson from "superjson";
|
||||||
|
|
||||||
import { useTranslation } from "@/i18n/client";
|
import { useTranslation } from "@/i18n/client";
|
||||||
|
|
||||||
|
import { toast } from "@rallly/ui/sonner";
|
||||||
import { trpc } from "../client";
|
import { trpc } from "../client";
|
||||||
|
|
||||||
export function TRPCProvider(props: { children: React.ReactNode }) {
|
export function TRPCProvider(props: { children: React.ReactNode }) {
|
||||||
const posthog = usePostHog();
|
const posthog = usePostHog();
|
||||||
const { toast } = useToast();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [queryClient] = useState(
|
const [queryClient] = useState(
|
||||||
() =>
|
() =>
|
||||||
|
@ -41,14 +40,16 @@ export function TRPCProvider(props: { children: React.ReactNode }) {
|
||||||
window.location.href = "/login";
|
window.location.href = "/login";
|
||||||
break;
|
break;
|
||||||
case "TOO_MANY_REQUESTS":
|
case "TOO_MANY_REQUESTS":
|
||||||
toast({
|
toast.error(
|
||||||
title: t("tooManyRequests", {
|
t("tooManyRequests", {
|
||||||
defaultValue: "Too many requests",
|
defaultValue: "Too many requests",
|
||||||
}),
|
}),
|
||||||
description: t("tooManyRequestsDescription", {
|
{
|
||||||
defaultValue: "Please try again later.",
|
description: t("tooManyRequestsDescription", {
|
||||||
}),
|
defaultValue: "Please try again later.",
|
||||||
});
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "FORBIDDEN":
|
case "FORBIDDEN":
|
||||||
signOut({
|
signOut({
|
||||||
|
|
|
@ -1,190 +0,0 @@
|
||||||
// Inspired by react-hot-toast library
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import type { ToastActionElement, ToastProps } from "../toast";
|
|
||||||
|
|
||||||
const TOAST_LIMIT = 1;
|
|
||||||
const TOAST_REMOVE_DELAY = 1000000;
|
|
||||||
|
|
||||||
type ToasterToast = ToastProps & {
|
|
||||||
id: string;
|
|
||||||
title?: React.ReactNode;
|
|
||||||
description?: React.ReactNode;
|
|
||||||
action?: ToastActionElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
function genId() {
|
|
||||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
|
||||||
return count.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActionType = {
|
|
||||||
ADD_TOAST: "ADD_TOAST";
|
|
||||||
UPDATE_TOAST: "UPDATE_TOAST";
|
|
||||||
DISMISS_TOAST: "DISMISS_TOAST";
|
|
||||||
REMOVE_TOAST: "REMOVE_TOAST";
|
|
||||||
};
|
|
||||||
|
|
||||||
type Action =
|
|
||||||
| {
|
|
||||||
type: ActionType["ADD_TOAST"];
|
|
||||||
toast: ToasterToast;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ActionType["UPDATE_TOAST"];
|
|
||||||
toast: Partial<ToasterToast>;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ActionType["DISMISS_TOAST"];
|
|
||||||
toastId?: ToasterToast["id"];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ActionType["REMOVE_TOAST"];
|
|
||||||
toastId?: ToasterToast["id"];
|
|
||||||
};
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
toasts: ToasterToast[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
||||||
|
|
||||||
const addToRemoveQueue = (toastId: string) => {
|
|
||||||
if (toastTimeouts.has(toastId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
toastTimeouts.delete(toastId);
|
|
||||||
dispatch({
|
|
||||||
type: "REMOVE_TOAST",
|
|
||||||
toastId: toastId,
|
|
||||||
});
|
|
||||||
}, TOAST_REMOVE_DELAY);
|
|
||||||
|
|
||||||
toastTimeouts.set(toastId, timeout);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const reducer = (state: State, action: Action): State => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "ADD_TOAST":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
|
||||||
};
|
|
||||||
|
|
||||||
case "UPDATE_TOAST":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: state.toasts.map((t) =>
|
|
||||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
case "DISMISS_TOAST": {
|
|
||||||
const { toastId } = action;
|
|
||||||
|
|
||||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
|
||||||
// but I'll keep it here for simplicity
|
|
||||||
if (toastId) {
|
|
||||||
addToRemoveQueue(toastId);
|
|
||||||
} else {
|
|
||||||
// biome-ignore lint/complexity/noForEach: Fix this later
|
|
||||||
state.toasts.forEach((toast) => {
|
|
||||||
addToRemoveQueue(toast.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: state.toasts.map((t) =>
|
|
||||||
t.id === toastId || toastId === undefined
|
|
||||||
? {
|
|
||||||
...t,
|
|
||||||
open: false,
|
|
||||||
}
|
|
||||||
: t,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "REMOVE_TOAST":
|
|
||||||
if (action.toastId === undefined) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const listeners: Array<(state: State) => void> = [];
|
|
||||||
|
|
||||||
let memoryState: State = { toasts: [] };
|
|
||||||
|
|
||||||
function dispatch(action: Action) {
|
|
||||||
memoryState = reducer(memoryState, action);
|
|
||||||
// biome-ignore lint/complexity/noForEach: Fix this later
|
|
||||||
listeners.forEach((listener) => {
|
|
||||||
listener(memoryState);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
type Toast = Omit<ToasterToast, "id">;
|
|
||||||
|
|
||||||
function toast({ ...props }: Toast) {
|
|
||||||
const id = genId();
|
|
||||||
|
|
||||||
const update = (props: ToasterToast) =>
|
|
||||||
dispatch({
|
|
||||||
type: "UPDATE_TOAST",
|
|
||||||
toast: { ...props, id },
|
|
||||||
});
|
|
||||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "ADD_TOAST",
|
|
||||||
toast: {
|
|
||||||
...props,
|
|
||||||
id,
|
|
||||||
open: true,
|
|
||||||
onOpenChange: (open) => {
|
|
||||||
if (!open) dismiss();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: id,
|
|
||||||
dismiss,
|
|
||||||
update,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function useToast() {
|
|
||||||
const [state, setState] = React.useState<State>(memoryState);
|
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: I think this needs to be here
|
|
||||||
React.useEffect(() => {
|
|
||||||
listeners.push(setState);
|
|
||||||
return () => {
|
|
||||||
const index = listeners.indexOf(setState);
|
|
||||||
if (index > -1) {
|
|
||||||
listeners.splice(index, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [state]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
toast,
|
|
||||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { toast, useToast };
|
|
|
@ -1,30 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { useToast } from "./hooks/use-toast";
|
|
||||||
import {
|
|
||||||
Toast,
|
|
||||||
ToastClose,
|
|
||||||
ToastDescription,
|
|
||||||
ToastProvider,
|
|
||||||
ToastTitle,
|
|
||||||
ToastViewport,
|
|
||||||
} from "./toast";
|
|
||||||
|
|
||||||
export function Toaster() {
|
|
||||||
const { toasts } = useToast();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToastProvider duration={2000}>
|
|
||||||
{toasts.map(({ id, title, description, action, ...props }) => (
|
|
||||||
<Toast key={id} {...props}>
|
|
||||||
<div className="grid gap-1">
|
|
||||||
{title && <ToastTitle>{title}</ToastTitle>}
|
|
||||||
{description && <ToastDescription>{description}</ToastDescription>}
|
|
||||||
</div>
|
|
||||||
{action}
|
|
||||||
<ToastClose />
|
|
||||||
</Toast>
|
|
||||||
))}
|
|
||||||
<ToastViewport />
|
|
||||||
</ToastProvider>
|
|
||||||
);
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue