Refactor toast

This commit is contained in:
Neil Dwyer 2024-08-15 13:06:24 -07:00
parent 3856f0cacc
commit 57b66f3f58
7 changed files with 106 additions and 79 deletions

29
package-lock.json generated
View File

@ -19,8 +19,7 @@
"next": "^14.0.4", "next": "^14.0.4",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18"
"react-hot-toast": "^2.4.1"
}, },
"devDependencies": { "devDependencies": {
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
@ -2008,7 +2007,8 @@
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true
}, },
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
@ -3073,14 +3073,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/goober": {
"version": "2.1.14",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz",
"integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@ -4609,21 +4601,6 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-hot-toast": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
"integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
"dependencies": {
"goober": "^2.1.10"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View File

@ -20,8 +20,7 @@
"next": "^14.0.4", "next": "^14.0.4",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18"
"react-hot-toast": "^2.4.1"
}, },
"devDependencies": { "devDependencies": {
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",

View File

@ -1,3 +1,5 @@
import { useToast } from "./ToasterProvider";
export type ToastType = "error" | "success" | "info"; export type ToastType = "error" | "success" | "info";
export type ToastProps = { export type ToastProps = {
message: string; message: string;
@ -5,16 +7,24 @@ export type ToastProps = {
onDismiss: () => void; onDismiss: () => void;
}; };
export const PlaygroundToast = ({ message, type, onDismiss }: ToastProps) => { export const PlaygroundToast = () => {
const { toastMessage, setToastMessage } = useToast();
const color = const color =
type === "error" ? "red" : type === "success" ? "green" : "amber"; toastMessage?.type === "error"
? "red"
: toastMessage?.type === "success"
? "green"
: "amber";
return ( return (
<div <div
className={`absolute text-sm break-words px-4 pr-12 py-2 bg-${color}-950 rounded-sm border border-${color}-800 text-${color}-400 top-4 left-4 right-4`} className={`absolute text-sm break-words px-4 pr-12 py-2 bg-${color}-950 rounded-sm border border-${color}-800 text-${color}-400 top-4 left-4 right-4`}
> >
<button <button
className={`absolute right-2 border border-transparent rounded-md px-2 hover:bg-${color}-900 hover:text-${color}-300`} className={`absolute right-2 border border-transparent rounded-md px-2 hover:bg-${color}-900 hover:text-${color}-300`}
onClick={onDismiss} onClick={() => {
setToastMessage(null);
}}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -31,7 +41,7 @@ export const PlaygroundToast = ({ message, type, onDismiss }: ToastProps) => {
/> />
</svg> </svg>
</button> </button>
{message} {toastMessage?.message}
</div> </div>
); );
}; };

View File

@ -0,0 +1,40 @@
"use client"
import React, { createContext, useState } from "react";
import { ToastType } from "./PlaygroundToast";
type ToastProviderData = {
setToastMessage: (
message: { message: string; type: ToastType } | null
) => void;
toastMessage: { message: string; type: ToastType } | null;
};
const ToastContext = createContext<ToastProviderData | undefined>(undefined);
export const ToastProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [toastMessage, setToastMessage] = useState<{message: string, type: ToastType} | null>(null);
return (
<ToastContext.Provider
value={{
toastMessage,
setToastMessage
}}
>
{children}
</ToastContext.Provider>
);
};
export const useToast = () => {
const context = React.useContext(ToastContext);
if (context === undefined) {
throw new Error("useToast must be used within a ToastProvider");
}
return context;
}

View File

@ -4,7 +4,7 @@ import { useCloud } from "@/cloud/useCloud";
import React, { createContext, useState } from "react"; import React, { createContext, useState } from "react";
import { useCallback } from "react"; import { useCallback } from "react";
import { useConfig } from "./useConfig"; import { useConfig } from "./useConfig";
import toast from "react-hot-toast"; import { useToast } from "@/components/toast/ToasterProvider";
export type ConnectionMode = "cloud" | "manual" | "env" export type ConnectionMode = "cloud" | "manual" | "env"
@ -25,6 +25,7 @@ export const ConnectionProvider = ({
children: React.ReactNode; children: React.ReactNode;
}) => { }) => {
const { generateToken, wsUrl: cloudWSUrl } = useCloud(); const { generateToken, wsUrl: cloudWSUrl } = useCloud();
const { setToastMessage } = useToast();
const { config } = useConfig(); const { config } = useConfig();
const [connectionDetails, setConnectionDetails] = useState<{ const [connectionDetails, setConnectionDetails] = useState<{
wsUrl: string; wsUrl: string;
@ -33,16 +34,19 @@ export const ConnectionProvider = ({
shouldConnect: boolean; shouldConnect: boolean;
}>({ wsUrl: "", token: "", shouldConnect: false, mode: "manual" }); }>({ wsUrl: "", token: "", shouldConnect: false, mode: "manual" });
const connect = useCallback(async (mode: ConnectionMode) => { const connect = useCallback(
async (mode: ConnectionMode) => {
let token = ""; let token = "";
let url = ""; let url = "";
if (mode === "cloud") { if (mode === "cloud") {
try { try {
token = await generateToken(); token = await generateToken();
} catch (error) { } catch (error) {
toast.error( setToastMessage({
"Failed to generate token, you may need to increase your role in this LiveKit Cloud project." type: "error",
); message:
"Failed to generate token, you may need to increase your role in this LiveKit Cloud project.",
});
} }
url = cloudWSUrl; url = cloudWSUrl;
} else if (mode === "env") { } else if (mode === "env") {
@ -50,19 +54,24 @@ export const ConnectionProvider = ({
throw new Error("NEXT_PUBLIC_LIVEKIT_URL is not set"); throw new Error("NEXT_PUBLIC_LIVEKIT_URL is not set");
} }
url = process.env.NEXT_PUBLIC_LIVEKIT_URL; url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
const {accessToken} = await fetch("/api/token").then((res) => res.json()); const { accessToken } = await fetch("/api/token").then((res) =>
res.json()
);
token = accessToken; token = accessToken;
} else { } else {
token = config.settings.token; token = config.settings.token;
url = config.settings.ws_url; url = config.settings.ws_url;
} }
setConnectionDetails({ wsUrl: url, token, shouldConnect: true, mode }); setConnectionDetails({ wsUrl: url, token, shouldConnect: true, mode });
}, [ },
[
cloudWSUrl, cloudWSUrl,
config.settings.token, config.settings.token,
config.settings.ws_url, config.settings.ws_url,
generateToken, generateToken,
]); setToastMessage,
]
);
const disconnect = useCallback(async () => { const disconnect = useCallback(async () => {
setConnectionDetails((prev) => ({ ...prev, shouldConnect: false })); setConnectionDetails((prev) => ({ ...prev, shouldConnect: false }));

View File

@ -1,12 +1,10 @@
import { CloudProvider } from "@/cloud/useCloud"; import { CloudProvider } from "@/cloud/useCloud";
import "@/styles/globals.css"; import "@/styles/globals.css";
import type { AppProps } from "next/app"; import type { AppProps } from "next/app";
import { Toaster } from "react-hot-toast";
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
return ( return (
<CloudProvider> <CloudProvider>
<Toaster />
<Component {...pageProps} /> <Component {...pageProps} />
</CloudProvider> </CloudProvider>
);} );}

View File

@ -14,6 +14,7 @@ import { PlaygroundToast, ToastType } from "@/components/toast/PlaygroundToast";
import { ConfigProvider, useConfig } from "@/hooks/useConfig"; import { ConfigProvider, useConfig } from "@/hooks/useConfig";
import { ConnectionMode, ConnectionProvider, useConnection } from "@/hooks/useConnection"; import { ConnectionMode, ConnectionProvider, useConnection } from "@/hooks/useConnection";
import { useMemo } from "react"; import { useMemo } from "react";
import { ToastProvider, useToast } from "@/components/toast/ToasterProvider";
const themeColors = [ const themeColors = [
"cyan", "cyan",
@ -30,23 +31,22 @@ const inter = Inter({ subsets: ["latin"] });
export default function Home() { export default function Home() {
return ( return (
<ToastProvider>
<ConfigProvider> <ConfigProvider>
<ConnectionProvider> <ConnectionProvider>
<HomeInner /> <HomeInner />
</ConnectionProvider> </ConnectionProvider>
</ConfigProvider> </ConfigProvider>
</ToastProvider>
); );
} }
export function HomeInner() { export function HomeInner() {
const [toastMessage, setToastMessage] = useState<{
message: string;
type: ToastType;
} | null>(null);
const { shouldConnect, wsUrl, token, mode, connect, disconnect } = const { shouldConnect, wsUrl, token, mode, connect, disconnect } =
useConnection(); useConnection();
const {config} = useConfig(); const {config} = useConfig();
const { toastMessage, setToastMessage } = useToast();
const handleConnect = useCallback( const handleConnect = useCallback(
async (c: boolean, mode: ConnectionMode) => { async (c: boolean, mode: ConnectionMode) => {
@ -93,13 +93,7 @@ export function HomeInner() {
animate={{ opacity: 1, translateY: 0 }} animate={{ opacity: 1, translateY: 0 }}
exit={{ opacity: 0, translateY: -50 }} exit={{ opacity: 0, translateY: -50 }}
> >
<PlaygroundToast <PlaygroundToast />
message={toastMessage.message}
type={toastMessage.type}
onDismiss={() => {
setToastMessage(null);
}}
/>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>