Skip to content

Commit

Permalink
creates modular toast component (gitcoinco#39)
Browse files Browse the repository at this point in the history
* creates modular toast component

* chore: extended tailwind theme

* chore: toast - changed styles and added override of close button

* removed shadcn toast

* seperated toast and toaster migrated story to components/toaster

* fixed interface nicer

---------

Co-authored-by: Hussein Martinez <husse.dev@gmail.com>
  • Loading branch information
nijoe1 and hussedev authored Nov 19, 2024
1 parent 438dbd1 commit 5e3cba1
Show file tree
Hide file tree
Showing 13 changed files with 549 additions and 158 deletions.
4 changes: 4 additions & 0 deletions src/assets/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Status Icons
export { default as CheckIcon } from "./check.svg?react";
// Solid Check Icon
export { default as CheckSolidIcon } from "./solid/check.svg?react";
export { default as ExclamationCircleIcon } from "./exclamationCircle.svg?react";
export { default as XIcon } from "./x.svg?react";
// Solid X Icon
export { default as XSolidIcon } from "./solid/x.svg?react";

// Time Icons
export { default as ClockIcon } from "./clock.svg?react";
Expand Down
3 changes: 3 additions & 0 deletions src/assets/icons/solid/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/solid/x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
87 changes: 87 additions & 0 deletions src/components/Toaster/Toaster.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Meta } from "@storybook/blocks";

import * as ToasterStories from "./Toaster.stories";

<Meta of={ToasterStories} />

# Toaster Component

The `Toaster` component works with the `useToast` hook to display toast notifications in your app.

## Usage

1. **Add the Toaster**: Place the `Toaster` at the root of your app.

```tsx
import { Toaster } from "./Toaster";

function App() {
return (
<>
<Toaster />
</>
);
}
```

2. **Trigger a Toast**: Use the `useToast` hook in your components.

```tsx
import { useToast } from "@/hooks/use-toast";
import { Button } from "@/primitives/Button";

function TriggerButton() {
const { toast } = useToast();

const triggerToast = () => {
toast({
status: "error", // Variant of the toast (success, error, warning, info)
description: "Something went wrong! Please try again.", // Message content
timeout: 5000, // Duration before auto-dismissal
toastPosition: "bottom-right", // Where the toast appears on screen
toastSize: "large", // Size of the toast (small, medium, large)
descriptionSize: "large", // Size of the description text
toastCloseVariant: "alwaysVisible", // Style for the close action
});
};
}
```

# ToasterStories Component

The `ToasterStories` component is a flexible UI element used to display brief messages to users. It can be customized in various ways to suit different needs.

## Toast Component Variations

### Status Variations

- **Success**: Indicates a successful operation.
- **Error**: Represents an error or failure.
- **Info**: Provides informational messages. `(WIP)`
- **Warning**: Alerts the user to a potential issue. `(WIP)`

### Description

- A string that provides additional context or information about the toast message.

### Timeout

- An optional number specifying how long the toast should be visible before automatically dismissing.

### Position Variations (`toastPosition`)

- Determines where on the viewport the toast will appear. The position is defined by the keys of the `viewportVariants.variants.position` object.

### Description Size Variations (`descriptionSize`)

- Specifies the size of the description text, based on the keys of the `toastDescriptionVariants.variants.size` object.

### Toast Size Variations (`toastSize`)

- Defines the overall size of the toast, based on the keys of the `toastVariants.variants.size` object.

### Toast Close Variant (`toastCloseVariant`)

- Specifies the style of the close button, based on the keys of the `toastCloseVariants.variants.variant` object.

These variations allow the `Toast` component to be highly customizable, providing flexibility in both appearance and behavior to suit different use cases.
104 changes: 104 additions & 0 deletions src/components/Toaster/Toaster.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Toast.stories.tsx
import { Meta } from "@storybook/react";

import { useToast } from "@/hooks/use-toast";
import { Button } from "@/primitives/Button";

import { Toaster } from "./Toaster";

export default {
title: "components/Toaster",
decorators: [
(Story) => (
<>
<Story />
<Toaster />
</>
),
],
} as Meta;

export const SuccessToast = () => {
const { toast } = useToast();

const showToast = () => {
toast({
status: "success",
description: "Your evaluation has been saved",
timeout: 5000,
});
};
return <Button onClick={showToast} variant="primary" value="Show Default Success Toast" />;
};

export const ErrorToast = () => {
const { toast } = useToast();

const showToast = () => {
toast({
status: "error",
description: "Error: Your evaluation has not been saved. Please try again.",
timeout: 5000,
});
};

return <Button onClick={showToast} variant="primary" value="Show Default Error Toast" />;
};

// You can add more stories for different positions and variants as needed.

export const SuccessToastTopLeft = () => {
const { toast } = useToast();

const showToast = () => {
toast({
status: "success",
description: "Your evaluation has been saved",
timeout: 5000,
toastPosition: "top-left",
});
};
return <Button onClick={showToast} variant="primary" value="Show Success Toast Top Left" />;
};

export const ErrorToastTopRight = () => {
const { toast } = useToast();

const showToast = () => {
toast({
status: "error",
description: "Error: Your evaluation has not been saved. Please try again.",
timeout: 5000,
toastPosition: "top-right",
});
};
return <Button onClick={showToast} variant="primary" value="Show Error Toast Top Right" />;
};

export const SuccessToastBottomLeft = () => {
const { toast } = useToast();

const showToast = () => {
toast({
status: "success",
description: "Your evaluation has been saved",
timeout: 5000,
toastPosition: "bottom-left",
});
};
return <Button onClick={showToast} variant="primary" value="Show Success Toast Bottom Left" />;
};

export const ErrorToastTopCenter = () => {
const { toast } = useToast();

const showToast = () => {
toast({
status: "error",
description: "Error: Your evaluation has not been saved. Please try again.",
timeout: 5000,
toastPosition: "top-center",
});
};
return <Button onClick={showToast} variant="primary" value="Show Error Toast Bottom Right" />;
};
64 changes: 64 additions & 0 deletions src/components/Toaster/Toaster.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Toast.tsx
import { match } from "ts-pattern";

import { useToast, ToasterToast } from "@/hooks/use-toast";
import { Icon, IconType } from "@/primitives/Icon";
import {
Toast,
ToastProvider,
ToastViewport,
type viewportVariants,
} from "@/primitives/Toast/Toast";

const Toaster = () => {
const { toasts } = useToast();

// Group toasts by their toastPosition
const toastsByPosition = toasts.reduce(
(acc, toast) => {
const position = (toast.toastPosition || "bottom-right") as string;
if (!acc[position]) {
acc[position] = [];
}
acc[position].push(toast);
return acc;
},
{} as Record<string, ToasterToast[]>,
);

return (
<ToastProvider>
{Object.entries(toastsByPosition).map(([position, toasts]) => (
<ToastViewport
key={position}
position={position as keyof typeof viewportVariants.variants.position}
>
{toasts.map((toast) => {
const ToastIcon = match(toast.status)
.with("success", () => (
<Icon type={IconType.SOLID_CHECK} className="size-5 rounded-full" />
))
.with("error", () => <Icon type={IconType.SOLID_X} className="size-5 rounded-full" />)
// .with("info", () => (
// <Icon type={IconType.SOLID_INFO} className="size-5 rounded-full" />
// ))
// .with("warning", () => (
// <Icon type={IconType.SOLID_WARNING} className="size-5 rounded-full" />
// ))
.otherwise(() => <Icon type={IconType.SOLID_X} className="size-5 rounded-full" />);
return (
<Toast
toast={{
...toast,
icon: ToastIcon ?? IconType.SOLID_X,
}}
/>
);
})}
</ToastViewport>
))}
</ToastProvider>
);
};

export { Toaster };
31 changes: 25 additions & 6 deletions src/hooks/use-toast.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import * as React from "react";

import type { ToastActionElement, ToastProps } from "@/ui-shadcn/toast";
import type { ToastActionElement } from "@/primitives/Toast/Toast";
import { ToastProps } from "@/primitives/Toast/types";

const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;

type ToasterToast = ToastProps & {
export interface ToasterToast extends ToastProps {
id: string;
icon?: JSX.Element;
open?: boolean;
onOpenChange?: (open: boolean) => void;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
}

const actionTypes = {
ADD_TOAST: "ADD_TOAST",
Expand Down Expand Up @@ -121,7 +124,7 @@ export const reducer = (state: State, action: Action): State => {
}
};

const listeners: Array<(state: State) => void> = [];
const listeners: ((state: State) => void)[] = [];

let memoryState: State = { toasts: [] };

Expand All @@ -134,7 +137,16 @@ function dispatch(action: Action) {

type Toast = Omit<ToasterToast, "id">;

function toast({ ...props }: Toast) {
function toast({
status,
description,
timeout = 5000,
toastPosition = "bottom-right",
descriptionSize = "medium",
toastSize = "medium",
toastCloseVariant = "alwaysVisible",
...props
}: Toast) {
const id = genId();

const update = (props: ToasterToast) =>
Expand All @@ -149,6 +161,13 @@ function toast({ ...props }: Toast) {
toast: {
...props,
id,
status,
description,
timeout,
toastPosition,
descriptionSize,
toastSize,
toastCloseVariant,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
Expand Down
6 changes: 6 additions & 0 deletions src/primitives/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Status Icons
import {
CheckIcon,
CheckSolidIcon,
ClockIcon,
ExclamationCircleIcon,
SparklesIcon,
XIcon,
XSolidIcon,
CalendarIcon,
VerifiedBadgeIcon,
} from "@/assets/icons";
Expand All @@ -23,10 +25,12 @@ import {
export enum IconType {
// Status Icons
CHECK = "check",
SOLID_CHECK = "solid-check",
CLOCK = "clock",
EXCLAMATION_CIRCLE = "exclamation-circle",
SPARKLES = "sparkles",
X = "x",
SOLID_X = "solid-x",
CALENDAR = "calendar",
VERIFIEDBADGE = "verifiedBadge",

Expand All @@ -50,10 +54,12 @@ export type IconProps = React.SVGProps<SVGSVGElement> & {

const iconComponents: Record<IconProps["type"], React.FC<React.SVGProps<SVGSVGElement>>> = {
check: CheckIcon,
"solid-check": CheckSolidIcon,
clock: ClockIcon,
"exclamation-circle": ExclamationCircleIcon,
sparkles: SparklesIcon,
x: XIcon,
"solid-x": XSolidIcon,
twitter: TwitterIcon,
github: GithubIcon,
eth: ETHIcon,
Expand Down
Loading

0 comments on commit 5e3cba1

Please sign in to comment.