From 6213070739db2d098574be2f0d501efc4bc7d7dd Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Thu, 8 Feb 2024 16:19:54 -0800 Subject: [PATCH 1/2] feat: add a toaster for notifications and other snacks Use `react-hot-toast` to introduce "toaster" style notifications: https://react-hot-toast.com/ These should be refined over time, but as an experiment I've introduced this on the "plan change" page and am using it to let the user know when something goes wrong: or when something goes right: --- package.json | 1 + pnpm-lock.yaml | 25 +++++++++++++++++++++++++ src/app/layout.tsx | 2 ++ src/app/plans/change/page.tsx | 7 ++++++- src/components/Toaster.tsx | 27 +++++++++++++++++++++++++++ src/hooks.tsx | 4 ++-- src/toaster.ts | 20 ++++++++++++++++++++ 7 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 src/components/Toaster.tsx create mode 100644 src/toaster.ts diff --git a/package.json b/package.json index c7f86d4..493f058 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "next": "^13.5.4", "react": "latest", "react-dom": "latest", + "react-hot-toast": "^2.4.1", "swr": "^2.2.4" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12ff6de..1f6eab1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ dependencies: react-dom: specifier: latest version: 18.2.0(react@18.2.0) + react-hot-toast: + specifier: ^2.4.1 + version: 2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0) swr: specifier: ^2.2.4 version: 2.2.4(react@18.2.0) @@ -3499,6 +3502,14 @@ packages: slash: 3.0.0 dev: true + /goober@2.1.14(csstype@3.1.3): + resolution: {integrity: sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==} + peerDependencies: + csstype: ^3.0.10 + dependencies: + csstype: 3.1.3 + dev: false + /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: @@ -4789,6 +4800,20 @@ packages: scheduler: 0.23.0 dev: false + /react-hot-toast@2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + goober: 2.1.14(csstype@3.1.3) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - csstype + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7d033ea..015f0dd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import './globals.css' import type { Metadata } from 'next' import Provider from '@/components/W3UIProvider' +import Toaster from '@/components/Toaster' export const metadata: Metadata = { title: 'w3up console', @@ -18,6 +19,7 @@ export default function RootLayout ({ <>{children} + ) diff --git a/src/app/plans/change/page.tsx b/src/app/plans/change/page.tsx index 5afe939..81f2a8f 100644 --- a/src/app/plans/change/page.tsx +++ b/src/app/plans/change/page.tsx @@ -6,6 +6,7 @@ import { CheckCircleIcon } from '@heroicons/react/24/outline' import DefaultLoader from "@/components/Loader" import { useState } from "react" import SidebarLayout from "@/components/SidebarLayout" +import { ucantoast } from "@/toaster" interface PlanSectionProps { planID: DID @@ -38,7 +39,11 @@ function PlanSection ({ planID, planName, flatFee, flatFeeAllotment, perGbFee }: async function selectPlan (selectedPlanID: DID) { try { setIsUpdatingPlan(true) - await setPlan(selectedPlanID) + await ucantoast(setPlan(selectedPlanID), { + loading: "Updating plan...", + success: "Plan updated!", + error: "Failed to update plan, check the console for more details." + }) } finally { setIsUpdatingPlan(false) } diff --git a/src/components/Toaster.tsx b/src/components/Toaster.tsx new file mode 100644 index 0000000..a8ab05d --- /dev/null +++ b/src/components/Toaster.tsx @@ -0,0 +1,27 @@ +'use client' + +import { Toaster, ToastIcon, resolveValue } from "react-hot-toast" +import { Transition } from "@headlessui/react" + +export default function TailwindToaster () { + return ( + + {(t) => ( + + +

{resolveValue(t.message, t)}

+
+ )} +
+ ) +} \ No newline at end of file diff --git a/src/hooks.tsx b/src/hooks.tsx index 7e2c694..1656f54 100644 --- a/src/hooks.tsx +++ b/src/hooks.tsx @@ -11,7 +11,6 @@ type UsePlanResult = SWRResponse & { } export const usePlan = (account: Account) => { - const { mutate } = useSWRConfig() const result = useSWR(planKey(account), { fetcher: async () => { if (!account) return @@ -25,8 +24,9 @@ export const usePlan = (account: Account) => { // to avoid calling the getters in SWRResponse when copying values over - // I can't think of a cleaner way to do this but open to refactoring result.setPlan = async (plan: DID) => { - await account.plan.set(plan) + const setResult = await account.plan.set(plan) await result.mutate() + return setResult } return result as UsePlanResult } diff --git a/src/toaster.ts b/src/toaster.ts new file mode 100644 index 0000000..77fadad --- /dev/null +++ b/src/toaster.ts @@ -0,0 +1,20 @@ +import { Result } from '@ucanto/interface' +import { toast } from 'react-hot-toast' + +export const ucantoast = async (promise: Promise, options: any) => { + return toast.promise(new Promise(async (resolve, reject) => { + promise.then((result) => { + if (result.ok) { + resolve(result.ok) + } else { + console.error("toaster got a Ucanto error: ", result.error) + resolve(result.error) + } + }) + + promise.catch((err: unknown) => { + console.error("toaster caught an error: ", err) + reject(err) + }) + }), options) +} From c88f4ee66d9ce2262580c5615812532bef0f6310 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Tue, 16 Apr 2024 16:28:33 -0700 Subject: [PATCH 2/2] feat: much cleaner implementation thanks @alanshaw! --- src/toaster.ts | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/toaster.ts b/src/toaster.ts index 77fadad..93ecf72 100644 --- a/src/toaster.ts +++ b/src/toaster.ts @@ -2,19 +2,12 @@ import { Result } from '@ucanto/interface' import { toast } from 'react-hot-toast' export const ucantoast = async (promise: Promise, options: any) => { - return toast.promise(new Promise(async (resolve, reject) => { - promise.then((result) => { - if (result.ok) { - resolve(result.ok) - } else { - console.error("toaster got a Ucanto error: ", result.error) - resolve(result.error) - } - }) - - promise.catch((err: unknown) => { - console.error("toaster caught an error: ", err) - reject(err) - }) - }), options) + return toast.promise((async () => { + const result = await promise + if (result.ok) { + return result.ok + } else { + throw result.error + } + })(), options) }