mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-05 18:09:04 +00:00
add react hot toast
This commit is contained in:
@@ -6,6 +6,7 @@ const config: StorybookConfig = {
|
||||
"../stories/SubmitButton.stories.tsx",
|
||||
"../stories/FormField.stories.tsx",
|
||||
"../stories/Navbar.stories.tsx",
|
||||
"../stories/Toast.stories.tsx",
|
||||
"../stories/Themes.stories.tsx",
|
||||
],
|
||||
addons: [
|
||||
|
||||
25
src/Website/app/components/toast/ToastProvider.tsx
Normal file
25
src/Website/app/components/toast/ToastProvider.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
export default function ToastProvider() {
|
||||
return (
|
||||
<Toaster
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
duration: 3500,
|
||||
className: "rounded-box border border-base-300 bg-base-100 text-base-content shadow-lg",
|
||||
success: {
|
||||
iconTheme: {
|
||||
primary: "var(--color-success)",
|
||||
secondary: "var(--color-success-content)",
|
||||
},
|
||||
},
|
||||
error: {
|
||||
iconTheme: {
|
||||
primary: "var(--color-error)",
|
||||
secondary: "var(--color-error-content)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
6
src/Website/app/components/toast/toast.ts
Normal file
6
src/Website/app/components/toast/toast.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export const showSuccessToast = (message: string) => toast.success(message);
|
||||
export const showErrorToast = (message: string) => toast.error(message);
|
||||
export const showInfoToast = (message: string) => toast(message);
|
||||
export const dismissToasts = () => toast.dismiss();
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import type { Route } from "./+types/root";
|
||||
import "./app.css";
|
||||
import Navbar from "./components/Navbar";
|
||||
import ToastProvider from "./components/toast/ToastProvider";
|
||||
import { getOptionalAuth } from "./lib/auth.server";
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
@@ -53,6 +54,7 @@ export default function App({ loaderData }: Route.ComponentProps) {
|
||||
return (
|
||||
<>
|
||||
<Navbar auth={auth} />
|
||||
<ToastProvider />
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
@@ -66,9 +68,7 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? "404" : "Error";
|
||||
details =
|
||||
error.status === 404
|
||||
? "The requested page could not be found."
|
||||
: error.statusText || details;
|
||||
error.status === 404 ? "The requested page could not be found." : error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { showErrorToast, showSuccessToast } from "../components/toast/toast";
|
||||
import { confirmEmail, requireAuth } from "../lib/auth.server";
|
||||
import type { Route } from "./+types/confirm";
|
||||
|
||||
@@ -30,6 +32,15 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
}
|
||||
|
||||
export default function Confirm({ loaderData }: Route.ComponentProps) {
|
||||
useEffect(() => {
|
||||
if (loaderData.success) {
|
||||
showSuccessToast("Email confirmed successfully.");
|
||||
return;
|
||||
}
|
||||
|
||||
showErrorToast(loaderData.error);
|
||||
}, [loaderData]);
|
||||
|
||||
return (
|
||||
<div className="hero min-h-screen bg-base-200">
|
||||
<div className="card w-full max-w-md bg-base-100 shadow-xl">
|
||||
@@ -63,8 +74,7 @@ export default function Confirm({ loaderData }: Route.ComponentProps) {
|
||||
<span>{loaderData.error}</span>
|
||||
</div>
|
||||
<p className="text-base-content/70 text-sm">
|
||||
The confirmation link may have expired (valid for 30 minutes) or
|
||||
already been used.
|
||||
The confirmation link may have expired (valid for 30 minutes) or already been used.
|
||||
</p>
|
||||
<div className="card-actions w-full pt-2 flex-col gap-2">
|
||||
<Link to="/dashboard" className="btn btn-primary w-full">
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { HomeSimpleDoor, LogIn, UserPlus } from "iconoir-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, redirect, useNavigation, useSubmit } from "react-router";
|
||||
import FormField from "../components/forms/FormField";
|
||||
import SubmitButton from "../components/forms/SubmitButton";
|
||||
import { showErrorToast } from "../components/toast/toast";
|
||||
import { createAuthSession, getOptionalAuth, login } from "../lib/auth.server";
|
||||
import { loginSchema, type LoginSchema } from "../lib/schemas";
|
||||
import type { Route } from "./+types/login";
|
||||
@@ -52,6 +54,12 @@ export default function Login({ actionData }: Route.ComponentProps) {
|
||||
submit(data, { method: "post" });
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (actionData?.error) {
|
||||
showErrorToast(actionData.error);
|
||||
}
|
||||
}, [actionData?.error]);
|
||||
|
||||
return (
|
||||
<div className="hero min-h-screen bg-base-200">
|
||||
<div className="card w-full max-w-md bg-base-100 shadow-xl">
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, redirect, useNavigation, useSubmit } from "react-router";
|
||||
import FormField from "../components/forms/FormField";
|
||||
import SubmitButton from "../components/forms/SubmitButton";
|
||||
import { showErrorToast } from "../components/toast/toast";
|
||||
import { createAuthSession, getOptionalAuth, register } from "../lib/auth.server";
|
||||
import { registerSchema, type RegisterSchema } from "../lib/schemas";
|
||||
import type { Route } from "./+types/register";
|
||||
@@ -68,6 +70,12 @@ export default function Register({ actionData }: Route.ComponentProps) {
|
||||
submit(data, { method: "post" });
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (actionData?.error) {
|
||||
showErrorToast(actionData.error);
|
||||
}
|
||||
}, [actionData?.error]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200 flex items-center justify-center p-4">
|
||||
<div className="card w-full max-w-lg bg-base-100 shadow-xl">
|
||||
|
||||
28
src/Website/package-lock.json
generated
28
src/Website/package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.71.2",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-router": "^7.13.1",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
@@ -3933,7 +3934,6 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
@@ -4875,6 +4875,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/goober": {
|
||||
"version": "2.1.18",
|
||||
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
|
||||
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"csstype": "^3.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -6385,6 +6394,23 @@
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
||||
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.3",
|
||||
"goober": "^2.1.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-dom": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.71.2",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-router": "^7.13.1",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
|
||||
63
src/Website/stories/Toast.stories.tsx
Normal file
63
src/Website/stories/Toast.stories.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, screen, userEvent, within } from "storybook/test";
|
||||
import ToastProvider from "../app/components/toast/ToastProvider";
|
||||
import {
|
||||
dismissToasts,
|
||||
showErrorToast,
|
||||
showInfoToast,
|
||||
showSuccessToast,
|
||||
} from "../app/components/toast/toast";
|
||||
|
||||
function ToastDemo() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<ToastProvider />
|
||||
<div className="card border border-base-300 bg-base-100 shadow-md">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">Toast demo</h2>
|
||||
<p className="text-sm text-base-content/70">Use these actions to preview toast styles.</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
className="btn btn-success btn-sm"
|
||||
onClick={() => showSuccessToast("Saved successfully")}
|
||||
>
|
||||
Success
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-error btn-sm"
|
||||
onClick={() => showErrorToast("Something went wrong")}
|
||||
>
|
||||
Error
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-info btn-sm"
|
||||
onClick={() => showInfoToast("Heads up: check your email")}
|
||||
>
|
||||
Info
|
||||
</button>
|
||||
<button className="btn btn-ghost btn-sm" onClick={dismissToasts}>
|
||||
Dismiss all
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Feedback/Toast",
|
||||
component: ToastDemo,
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof ToastDemo>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Playground: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(canvas.getByRole("button", { name: /success/i }));
|
||||
await expect(screen.getByText(/saved successfully/i)).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user