Skip to content

Commit

Permalink
Add message toast
Browse files Browse the repository at this point in the history
  • Loading branch information
slhmy committed Apr 13, 2024
1 parent 18992ee commit ce7562c
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 25 deletions.
71 changes: 48 additions & 23 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.0.18",
"@mui/icons-material": "^5.10.6",
"@reduxjs/toolkit": "^1.8.6",
"@reduxjs/toolkit": "^2.2.3",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.5.2",
Expand Down Expand Up @@ -36,6 +36,7 @@
"rehype-mathjax": "^4.0.3",
"remark-math": "^5.1.1",
"typescript": "^5.3.3",
"uuid": "^9.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
Expand Down Expand Up @@ -74,6 +75,7 @@
"@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-syntax-highlighter": "^15.5.11",
"@types/redux-logger": "^3.0.9",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.15",
"daisyui": "^4.7.3",
Expand Down
4 changes: 4 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

.fade-out {
@apply animate-fadeOut;
}
6 changes: 5 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import reportWebVitals from "./reportWebVitals";
import "./i18n/i18n";
import "./index.css";
import { getMode, isGhPages, isMock } from "./utils/environment";
import { Provider } from "react-redux";
import store from "./store";

console.log("Running in:", getMode());

Expand Down Expand Up @@ -36,7 +38,9 @@ enableMocking().then(() => {
);
root.render(
<React.StrictMode>
<App />
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
);
});
Expand Down
45 changes: 45 additions & 0 deletions src/layouts/userLayout/UserLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
import { useDispatch, useSelector } from "react-redux";
import Navbar from "./Navbar";
import { useEffect } from "react";
import { messageMapSelector } from "@/store/slices/message";
import { ADD_MESSAGE_SAGA } from "@/store/sagas/message";
import { joinClasses } from "@/utils/common";

export interface UserLayoutProps {
title?: string;
children?: React.ReactNode;
}

const UserLayout: React.FC<UserLayoutProps> = (props) => {
let messageMap = useSelector(messageMapSelector);
let dispatch = useDispatch();

useEffect(() => {
dispatch({
type: ADD_MESSAGE_SAGA,
payload: {
id: "welcome",
content: "🥰 Welcome to OJ Lab",
duration: 3000,
},
});
setTimeout(() => {
dispatch({
type: ADD_MESSAGE_SAGA,
payload: {
id: "message",
content: "🕛 Message will fade out after several seconds",
duration: 3000,
},
});
}, 1000);
}, [dispatch]);

return (
<div className="relative flex flex-1 flex-col">
<Navbar />
Expand All @@ -19,6 +48,22 @@ const UserLayout: React.FC<UserLayoutProps> = (props) => {
<main className="w-full max-w-7xl flex-auto flex-col self-center py-6 sm:px-6 lg:px-8">
{props.children}
</main>
<div className="toast toast-end toast-bottom ">
{Object.keys(messageMap).map((key) => {
let message = messageMap[key];
return (
<div
key={key}
className={joinClasses(
"neutral-content alert w-80 whitespace-normal font-bold",
message.exiting ? "fade-out" : "",
)}
>
{message.content}
</div>
);
})}
</div>
</div>
);
};
Expand Down
26 changes: 26 additions & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { configureStore, Middleware } from "@reduxjs/toolkit";
import rootReducers from "./reducer";
import logger from "redux-logger";
import createSagaMiddleware from "redux-saga";
import rootSaga from "./saga";

const sagaMiddleware = createSagaMiddleware();
const middlewares: Middleware[] = [sagaMiddleware];

if (process.env.NODE_ENV === "development") {
middlewares.push(logger);
}

const store = configureStore({
reducer: rootReducers,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(...middlewares),
});

sagaMiddleware.run(rootSaga);

export type RootState = ReturnType<typeof store.getState>;

export type AppDispatch = typeof store.dispatch;

export default store;
8 changes: 8 additions & 0 deletions src/store/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { combineReducers } from "@reduxjs/toolkit";
import message from "./slices/message";

const rootReducers = combineReducers({
messageReducer: message,
});

export default rootReducers;
23 changes: 23 additions & 0 deletions src/store/saga.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { all, call, spawn } from "redux-saga/effects";
import { watchAddMessage } from "./sagas/message";

export default function* rootSaga() {
let sagas = [watchAddMessage];

// Spawn a detached generater for each 'watcher' saga, that is meant to stay alive for the entire app life-time
// Will catch any otherwise uncaught errors in sagas, and restart those crashed sagas.
yield all(
sagas.map((saga) =>
spawn(function* () {
while (true) {
try {
yield call(saga);
break;
} catch (e) {
console.log(e);
}
}
}),
),
);
}
25 changes: 25 additions & 0 deletions src/store/sagas/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { PayloadAction } from "@reduxjs/toolkit";
import {
Message,
addMessage,
removeMessage,
setMessageExiting,
} from "../slices/message";
import { put, takeEvery } from "redux-saga/effects";
import { v4 as uuidv4 } from "uuid";

function* addMessageSaga(action: PayloadAction<Message>) {
action.payload.id = action.payload.id || uuidv4();
yield put(addMessage(action.payload));
// Remove the message after the duration
yield new Promise((resolve) => setTimeout(resolve, action.payload.duration));
yield put(setMessageExiting(action.payload.id));
yield new Promise((resolve) => setTimeout(resolve, 1000));
yield put(removeMessage(action.payload.id));
}

export const ADD_MESSAGE_SAGA = "message/addMessage/saga";

export function* watchAddMessage() {
yield takeEvery("message/addMessage/saga", addMessageSaga);
}
45 changes: 45 additions & 0 deletions src/store/slices/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { RootState } from "..";

export interface Message {
id: string;
content: string;
exiting?: boolean;
duration: number;
}

export interface MessageState {
messageMap: { [key: string]: Message };
}

const initialState: MessageState = {
messageMap: {},
};

const messageSlice = createSlice({
name: "message",
initialState,
reducers: {
addMessage(state, action: PayloadAction<Message>) {
let addingMessage = action.payload;
state.messageMap[addingMessage.id] = addingMessage;
},
setMessageExiting(state, action: PayloadAction<string>) {
let exitingMessage = state.messageMap[action.payload];
if (exitingMessage) {
exitingMessage.exiting = true;
}
},
removeMessage(state, action: PayloadAction<string>) {
delete state.messageMap[action.payload];
},
},
});

export const messageMapSelector = (state: RootState) =>
state.messageReducer.messageMap;

export const { addMessage, setMessageExiting, removeMessage } =
messageSlice.actions;

export default messageSlice.reducer;
Loading

0 comments on commit ce7562c

Please sign in to comment.