Skip to content

Commit

Permalink
client:Design a Toast Component
Browse files Browse the repository at this point in the history
This commit creates the toast component with customizable position,duration
and color.
In main.tsx wrapped the app component in toastcontextprovider so that
toast can be used everywhere in the app.

fixes:#68
  • Loading branch information
ritwik-69 authored and kuv2707 committed Jun 7, 2024
1 parent 19abe9b commit 8dc5449
Show file tree
Hide file tree
Showing 4 changed files with 343 additions and 1 deletion.
138 changes: 138 additions & 0 deletions frontend/src/library/toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ToastContext } from './toast-context';
import './toast.css';

function useTimeout(callback: () => void, duration: number) {
const savedCallback = useRef(callback);

useEffect(() => {
savedCallback.current = callback;
}, [callback]);

useEffect(() => {
const functionId = setTimeout(() => savedCallback.current(), duration);

return () => {
clearTimeout(functionId);
};
}, [duration]);
}
type toastProperties = {
message: string;
close: () => void;
duration: number;
position: string;
color: string;
};

export function Toast({
message,
close,
duration,
position,
color,
}: toastProperties) {
useTimeout(() => {
close();
}, duration);
return (
<div className={`toast ${position}-animation ${color}`}>
<p>{message}</p>
<button className="close-button" onClick={close}>
{'x'}
</button>
</div>
);
}

type ToastProviderProperties = {
children: React.ReactElement;
};
type ToastType = {
message: string;
id: number;
duration: number;
position: string;
color: string;
};

export function ToastProvider({ children }: ToastProviderProperties) {
const [toasts, setToasts] = useState<ToastType[]>([]);
const [position, setPosition] = useState('top-left');
type Options = {
message?: string;
duration?: number;
position?: string;
color?: 'info' | 'warning' | 'error' | 'success';
};
const openToast = useCallback(
({
message = '',
duration = 5000,
position = 'top-center',
color = 'info',
}: Options = {}) => {
const newToast = {
message: message,
id: Date.now(),
duration: duration,
position: position,
color: color,
};
setToasts((prevToast) => [...prevToast, newToast]);
setPosition(position);
},
[]
);

const closeToast = useCallback((id: number) => {
setTimeout(() => {
setToasts((prevToasts) =>
prevToasts.filter((toast) => toast.id !== id)
);
}, 300);

setToasts((toasts) => {
return toasts.map((toast) => {
if (toast.id === id) {
if (toast.position == 'top-left')
toast.position = 'fade-out-left';
else if (toast.position == 'top-right')
toast.position = 'fade-out-right';
else if (toast.position == 'top-center')
toast.position = 'fade-out-center';
}
return toast;
});
});
}, []);
const contextValue = useMemo(
() => ({
open: openToast,
close: closeToast,
}),
[openToast, closeToast]
);
return (
<ToastContext.Provider value={contextValue}>
{children}
<div className={`toasts ${position}`}>
{toasts &&
toasts.map((toast) => {
return (
<Toast
key={toast.id}
message={toast.message}
close={() => {
closeToast(toast.id);
}}
duration={toast.duration}
position={toast.position}
color={toast.color}
/>
);
})}
</div>
</ToastContext.Provider>
);
}
20 changes: 20 additions & 0 deletions frontend/src/library/toast/toast-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createContext, useContext } from 'react';

type Options = {
message?: string;
duration?: number;
position?: string;
color?: 'info' | 'warning' | 'error' | 'success';
};

type ToastContextValue = {
open: (options?: Options) => void;
close: (id: number) => void;
};

export const ToastContext = createContext<ToastContextValue>({
open: () => {},
close: () => {},
});

export const useToast = () => useContext(ToastContext);
181 changes: 181 additions & 0 deletions frontend/src/library/toast/toast.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
.toasts {
display: flex;
flex-direction: column;
gap: 10px;
}

.toast {
color: black;
border-radius: 5px;
padding: 10px 10px;
width: 300px;
position: relative;
display: flex;
}

.top-right {
position: fixed;
top: 10px;
right: 10px;
}

.top-left {
position: fixed;
top: 10px;
left: 10px;
}
.top-center {
position: fixed;
top: 10px;
left: 38%;
}

.top-center-animation {
animation-name: slideinCenter;
animation-duration: 0.35s;
}

.top-right-animation {
animation-name: slideinRight;
animation-duration: 0.35s;
}

.top-left-animation {
animation-name: slideinLeft;
animation-duration: 0.35s;
}

@keyframes slideinRight {
0% {
transform: translateX(100%);
}
60% {
transform: translateX(-15%);
}
80% {
transform: translateX(5%);
}
80% {
transform: translateX(0);
}
}
@keyframes slideinCenter {
0% {
transform: translateY(-100%);
}
60% {
transform: translateY(15%);
}
80% {
transform: translateY(-5%);
}
80% {
transform: translateY(0);
}
}

@keyframes slideinLeft {
0% {
transform: translateX(-100%);
}
60% {
transform: translateX(15%);
}
80% {
transform: translateX(-5%);
}
80% {
transform: translateX(0);
}
}

.fade-out-left-animation {
animation-name: fade-out-left;
animation-duration: 0.35s;
}

.fade-out-right-animation {
animation-name: fade-out-right;
animation-duration: 0.35s;
}

.fade-out-center-animation {
animation-name: fade-out-center;
animation-duration: 0.35s;
}

@keyframes fade-out-left {
0% {
transform: translateX(0%);
}

60% {
transform: translateX(-100%);
}

80% {
transform: translateX(-195%);
}

100% {
transform: translateX(-200%);
}
}
@keyframes fade-out-right {
0% {
transform: translateX(0%);
}

60% {
transform: translateX(100%);
}

80% {
transform: translateX(195%);
}

100% {
transform: translateX(200%);
}
}

@keyframes fade-out-center {
0% {
transform: translateY(-100%);
}
30% {
transform: translateY(-300%);
}
80% {
transform: translateY(-700%);
}
100% {
transform: translateY(-1000%);
}
}

.info {
background-color: cyan;
}

.success {
background-color: #5cb85c;
}

.error {
background-color: #d9534f;
}
.warning {
background-color: #f0ad4e;
}

.close-button {
position: absolute;
right: 0px;
top: 0px;
padding: 0px 5px;
background: none;
cursor: pointer;
border: transparent;
color: black;
}
5 changes: 4 additions & 1 deletion frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import { ToastProvider } from './library/toast/Toast.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
<ToastProvider>
<App />
</ToastProvider>
</React.StrictMode>
);

0 comments on commit 8dc5449

Please sign in to comment.