Skip to content

Commit 1906d39

Browse files
author
Travis Vachon
authored
feat: add a toaster for notifications and other snacks (#91)
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: https://github.com/web3-storage/console/assets/1113/ffe3c1cf-5a42-4471-920c-649bfecb36a4 or when something goes right: https://github.com/web3-storage/console/assets/1113/af7f5bd2-cf20-482c-803c-22d5804ed86a If this seems like a good pattern to everyone I think we should start using it more broadly - no longer will our users be forced to watch a whole file upload just to get feedback about its success or failure!
1 parent a6d75ae commit 1906d39

File tree

7 files changed

+76
-3
lines changed

7 files changed

+76
-3
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"next": "^13.5.4",
3232
"react": "latest",
3333
"react-dom": "latest",
34+
"react-hot-toast": "^2.4.1",
3435
"swr": "^2.2.4"
3536
},
3637
"devDependencies": {

pnpm-lock.yaml

+25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/layout.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import './globals.css'
22
import type { Metadata } from 'next'
33
import Provider from '@/components/W3UIProvider'
4+
import Toaster from '@/components/Toaster'
45

56
export const metadata: Metadata = {
67
title: 'w3up console',
@@ -18,6 +19,7 @@ export default function RootLayout ({
1819
<Provider>
1920
<>{children}</>
2021
</Provider>
22+
<Toaster />
2123
</body>
2224
</html>
2325
)

src/app/plans/change/page.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CheckCircleIcon } from '@heroicons/react/24/outline'
66
import DefaultLoader from "@/components/Loader"
77
import { useState } from "react"
88
import SidebarLayout from "@/components/SidebarLayout"
9+
import { ucantoast } from "@/toaster"
910

1011
interface PlanSectionProps {
1112
planID: DID
@@ -38,7 +39,11 @@ function PlanSection ({ planID, planName, flatFee, flatFeeAllotment, perGbFee }:
3839
async function selectPlan (selectedPlanID: DID) {
3940
try {
4041
setIsUpdatingPlan(true)
41-
await setPlan(selectedPlanID)
42+
await ucantoast(setPlan(selectedPlanID), {
43+
loading: "Updating plan...",
44+
success: "Plan updated!",
45+
error: "Failed to update plan, check the console for more details."
46+
})
4247
} finally {
4348
setIsUpdatingPlan(false)
4449
}

src/components/Toaster.tsx

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use client'
2+
3+
import { Toaster, ToastIcon, resolveValue } from "react-hot-toast"
4+
import { Transition } from "@headlessui/react"
5+
6+
export default function TailwindToaster () {
7+
return (
8+
<Toaster position="top-right">
9+
{(t) => (
10+
<Transition
11+
appear
12+
show={t.visible}
13+
className="transform p-4 flex bg-white rounded shadow-lg"
14+
enter="transition-all duration-150"
15+
enterFrom="opacity-0 scale-50"
16+
enterTo="opacity-100 scale-100"
17+
leave="transition-all duration-150"
18+
leaveFrom="opacity-100 scale-100"
19+
leaveTo="opacity-0 scale-75"
20+
>
21+
<ToastIcon toast={t} />
22+
<p className="px-2">{resolveValue(t.message, t)}</p>
23+
</Transition>
24+
)}
25+
</Toaster>
26+
)
27+
}

src/hooks.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ type UsePlanResult = SWRResponse<PlanGetSuccess | undefined> & {
1111
}
1212

1313
export const usePlan = (account: Account) => {
14-
const { mutate } = useSWRConfig()
1514
const result = useSWR<PlanGetSuccess | undefined>(planKey(account), {
1615
fetcher: async () => {
1716
if (!account) return
@@ -25,8 +24,9 @@ export const usePlan = (account: Account) => {
2524
// to avoid calling the getters in SWRResponse when copying values over -
2625
// I can't think of a cleaner way to do this but open to refactoring
2726
result.setPlan = async (plan: DID) => {
28-
await account.plan.set(plan)
27+
const setResult = await account.plan.set(plan)
2928
await result.mutate()
29+
return setResult
3030
}
3131
return result as UsePlanResult
3232
}

src/toaster.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Result } from '@ucanto/interface'
2+
import { toast } from 'react-hot-toast'
3+
4+
export const ucantoast = async (promise: Promise<Result>, options: any) => {
5+
return toast.promise((async () => {
6+
const result = await promise
7+
if (result.ok) {
8+
return result.ok
9+
} else {
10+
throw result.error
11+
}
12+
})(), options)
13+
}

0 commit comments

Comments
 (0)