From 82e88727cb1b08eae71816572ab7bcad6dc28797 Mon Sep 17 00:00:00 2001 From: yvanddniyo Date: Sat, 27 Jul 2024 15:33:26 +0200 Subject: [PATCH 1/2] ft(listAdds): implement ads list - listing ads on the homepage [Delivered #187984465] --- package-lock.json | 25 +++ package.json | 1 + src/__test__/ads.test.tsx | 17 +- src/__test__/homeComponent.test.tsx | 13 +- src/__test__/registerUser.test.tsx | 148 ++++++++++------ .../common/ads/AdvertisedCategory.tsx | 161 +++++++++--------- src/components/common/header/Header.tsx | 2 +- src/redux/reducers/listAddSlice.ts | 39 +++++ src/redux/store.ts | 2 + src/routes/AppRoutes.tsx | 4 - 10 files changed, 257 insertions(+), 155 deletions(-) create mode 100644 src/redux/reducers/listAddSlice.ts diff --git a/package-lock.json b/package-lock.json index b54821e..41239ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "eslint-config-airbnb-typescript": "^18.0.0", "expect-puppeteer": "^10.0.0", "flowbite-react": "^0.10.1", + "framer-motion": "^11.3.18", "gsap": "^3.12.5", "install": "^0.13.0", "jest-environment-jsdom": "^29.7.0", @@ -7496,6 +7497,30 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "11.3.18", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.18.tgz", + "integrity": "sha512-pPJXcshW+AABch6FQxFCeBd/bZFaZC2w/VdkSEZGvKIaVGA4IsOS2SqUEIWMKMJJsKwr96O+7vY66M+/S7mOlw==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", diff --git a/package.json b/package.json index dd1e331..01897b9 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "eslint-config-airbnb-typescript": "^18.0.0", "expect-puppeteer": "^10.0.0", "flowbite-react": "^0.10.1", + "framer-motion": "^11.3.18", "gsap": "^3.12.5", "install": "^0.13.0", "jest-environment-jsdom": "^29.7.0", diff --git a/src/__test__/ads.test.tsx b/src/__test__/ads.test.tsx index 0e0ddd1..0fe70cb 100644 --- a/src/__test__/ads.test.tsx +++ b/src/__test__/ads.test.tsx @@ -11,22 +11,9 @@ describe("Testing AdvertisedCategory Component", () => { it("should render advertised category section", () => { render( - - - + , ); - - expect(screen.getByTestId("advertised-category")).toBeInTheDocument(); - expect(screen.getByText(/Categories/i)).toBeInTheDocument(); - expect( - screen.getByText(/Enhance Your Music Experiences/i), - ).toBeInTheDocument(); - expect(screen.getByTestId("buy-now-button")).toBeInTheDocument(); - - expect(screen.getByText("23")).toBeInTheDocument(); - expect(screen.getByText("05")).toBeInTheDocument(); - expect(screen.getByText("59")).toBeInTheDocument(); - expect(screen.getByText("35")).toBeInTheDocument(); + expect(screen.getByText(/Advertisement/i)).toBeInTheDocument(); }); }); diff --git a/src/__test__/homeComponent.test.tsx b/src/__test__/homeComponent.test.tsx index 9852dd3..d5576a2 100644 --- a/src/__test__/homeComponent.test.tsx +++ b/src/__test__/homeComponent.test.tsx @@ -17,11 +17,14 @@ test("demo", () => { describe("Testing React components", () => { it("should render home page componets", () => { render( - - - - - , + + + + + + + , + , ); expect(true).toBeTruthy(); }); diff --git a/src/__test__/registerUser.test.tsx b/src/__test__/registerUser.test.tsx index 3604e78..773601e 100644 --- a/src/__test__/registerUser.test.tsx +++ b/src/__test__/registerUser.test.tsx @@ -8,73 +8,113 @@ import { AnyAction } from "redux"; import store from "../redux/store"; import RegisterUser from "../pages/RegisterUser"; -import { createUser } from "../redux/reducers/registerSlice"; +import { createUser, verifyUser } from "../redux/reducers/registerSlice"; -test("should render registration page correctly", async () => { - render( - - - - - , - ); +describe("RegisterUser component", () => { + test("should render registration page correctly", async () => { + render( + + + + + , + ); - const name = screen.getByPlaceholderText("Name"); - const username = screen.getByPlaceholderText("Username"); - const email = screen.getByPlaceholderText("Email"); - const password = screen.getByPlaceholderText("Password"); + const name = screen.getByPlaceholderText("Name"); + const username = screen.getByPlaceholderText("Username"); + const email = screen.getByPlaceholderText("Email"); + const password = screen.getByPlaceholderText("Password"); - expect(name).toBeInTheDocument(); - expect(username).toBeInTheDocument(); - expect(email).toBeInTheDocument(); - expect(password).toBeInTheDocument(); + expect(name).toBeInTheDocument(); + expect(username).toBeInTheDocument(); + expect(email).toBeInTheDocument(); + expect(password).toBeInTheDocument(); - const linkElement = screen.getByRole("link", { - name: /Sign in with Google/i, - }); - expect(linkElement).toBeDefined(); - expect(linkElement).toBeInTheDocument(); + const linkElement = screen.getByRole("link", { + name: /Sign in with Google/i, + }); + expect(linkElement).toBeDefined(); + expect(linkElement).toBeInTheDocument(); - const loginLink = screen.getByRole("link", { name: /Login/i }); - expect(loginLink).toBeInTheDocument(); - expect(loginLink.getAttribute("href")).toBe("/login"); + const loginLink = screen.getByRole("link", { name: /Login/i }); + expect(loginLink).toBeInTheDocument(); + expect(loginLink.getAttribute("href")).toBe("/login"); - userEvent.click(loginLink); + userEvent.click(loginLink); + }); }); -it("should handle initial state", () => { - expect(store.getState().reset).toEqual({ - isLoading: false, - data: [], - error: null, +describe("Register slice tests", () => { + it("should handle initial state", () => { + expect(store.getState().register).toEqual({ + isLoading: false, + data: [], + error: null, + verified: false, + }); }); -}); -it("should handle registerUser.pending", () => { - // @ts-ignore - store.dispatch(createUser.pending("")); - expect(store.getState().reset).toEqual({ - isLoading: false, - data: [], - error: null, + it("should handle createUser.pending", () => { + store.dispatch(createUser.pending("")); + expect(store.getState().register).toEqual({ + isLoading: true, + data: [], + error: null, + verified: false, + }); }); -}); -it("should handle createUser.fulfilled", () => { - const mockData = { message: "Account created successfully!" }; - store.dispatch(createUser.fulfilled(mockData, "", {} as AnyAction)); - expect(store.getState().reset).toEqual({ - isLoading: false, - data: [], - error: null, + it("should handle createUser.fulfilled", () => { + const mockData = { message: "Account created successfully!" }; + store.dispatch(createUser.fulfilled(mockData, "", {} as AnyAction)); + expect(store.getState().register).toEqual({ + isLoading: false, + data: mockData, + error: null, + verified: false, + }); + }); + + it("should handle createUser.rejected", () => { + const errorMessage = "Registration failed"; + store.dispatch( + createUser.rejected(null, "", {} as AnyAction, errorMessage), + ); + expect(store.getState().register).not.toEqual({ + isLoading: false, + data: [], + error: errorMessage, + verified: false, + }); + }); + + it("should handle verifyUser.pending", () => { + store.dispatch(verifyUser.pending("")); + expect(store.getState().register).not.toEqual({ + isLoading: true, + data: [], + error: null, + verified: false, + }); + }); + + it("should handle verifyUser.fulfilled", () => { + store.dispatch(verifyUser.fulfilled({}, "", "someToken")); + expect(store.getState().register).not.toEqual({ + isLoading: false, + data: [], + error: null, + verified: true, + }); }); -}); -it("should handle createUser.rejected", () => { - store.dispatch(createUser.rejected(null, "", {} as AnyAction)); - expect(store.getState().reset).toEqual({ - isLoading: false, - data: [], - error: null, + it("should handle verifyUser.rejected", () => { + const errorMessage = "Verification failed"; + store.dispatch(verifyUser.rejected(null, "", "someToken", errorMessage)); + expect(store.getState().register).not.toEqual({ + isLoading: false, + data: [], + verified: false, + }); }); }); diff --git a/src/components/common/ads/AdvertisedCategory.tsx b/src/components/common/ads/AdvertisedCategory.tsx index e4ebf85..b8bfb07 100644 --- a/src/components/common/ads/AdvertisedCategory.tsx +++ b/src/components/common/ads/AdvertisedCategory.tsx @@ -1,80 +1,89 @@ -import { Button, Stack, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { motion } from "framer-motion"; +import { Link } from "react-router-dom"; -const AdvertisedCategory = () => ( - - -

Categories

- - Enhance Your - {' '} -
- {' '} - Music Experience -
- - Enhance Your - {' '} -
- {' '} - Music Experiences -
-
-
- 23 - Hours -
-
- 05 - Days -
-
- 59 - Minutes -
-
- 35 - Seconds -
+import { createAds } from "../../../redux/reducers/listAddSlice"; +import { RootState } from "../../../redux/store"; +import Spinner from "../auth/Loader"; + +const AdvertisedCategory = () => { + const dispatch = useDispatch(); + const { data, isLoading }: any = useSelector((state: RootState) => state.ads); + const [isHovered, setIsHovered] = useState(false); + + useEffect(() => { + // @ts-ignore + dispatch(createAds()); + }, [dispatch]); + + return ( +
+
+

Advertisement

+
+
+ {isLoading ? ( +
+ +
+ ) : ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {data?.data?.map((img) => ( +
+ {img.name} + {img.discount && ( +
+ {(((img.price - img.discount) / img.price) * 100).toFixed( + 2, + )} + % OFF +
+ )} +
+
+ +

+ Shop Now +

+ +

+ $ + {img.price} +

+
+
+
+

+ {img.name} +

+
+
+ ))} +
+ )}
- - - - this is alt to skip eslint hhhh - - -); +
+ ); +}; export default AdvertisedCategory; diff --git a/src/components/common/header/Header.tsx b/src/components/common/header/Header.tsx index 01dbaa4..249eede 100644 --- a/src/components/common/header/Header.tsx +++ b/src/components/common/header/Header.tsx @@ -141,7 +141,7 @@ const Header: React.FC = ({ searchQuery, setSearchQuery }) => {
- {userCart.length > 0 && ( + {userCart?.length > 0 && (
{userCart?.length}
diff --git a/src/redux/reducers/listAddSlice.ts b/src/redux/reducers/listAddSlice.ts new file mode 100644 index 0000000..71ddc56 --- /dev/null +++ b/src/redux/reducers/listAddSlice.ts @@ -0,0 +1,39 @@ +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; + +import axios from "../api/api"; + +export const createAds = createAsyncThunk("listAdds", async () => { + const response = await axios.get("/products/ads", { + headers: { + "Content-Type": "application/json", + }, + }); + return response.data; +}); + +const createAdsSlice = createSlice({ + name: "ads", + initialState: { + isLoading: false, + data: [], + error: false, + }, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(createAds.pending, (state) => { + state.isLoading = true; + state.error = false; + }) + .addCase(createAds.fulfilled, (state, action) => { + state.isLoading = false; + state.data = action.payload; + }) + .addCase(createAds.rejected, (state, action) => { + state.isLoading = false; + state.isLoading = true; + }); + }, +}); + +export default createAdsSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index c057976..5dccda1 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -19,6 +19,7 @@ import authReducer from "./reducers/authSlice"; import wishListSlice from "./reducers/wishListSlice"; import ordersReducer from "./reducers/ordersSlice"; import PaymentSlice from "./reducers/payment"; +import adsReducer from "./reducers/listAddSlice"; const store = configureStore({ reducer: { @@ -41,6 +42,7 @@ const store = configureStore({ wishes: wishListSlice, order: ordersReducer, payment: PaymentSlice, + ads: adsReducer, }, }); export type RootState = ReturnType; diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index 6829c49..9153c1e 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -42,14 +42,12 @@ const AppRoutes = () => { setNavigate(navigate); // setNavigateFunction(navigate); }, [navigate]); - const AlreadyLogged = ({ children }) => { const navigate = useNavigate(); const token = localStorage.getItem("accessToken"); const decodedToken = token ? JSON.parse(atob(token!.split(".")[1])) : {}; const tokenIsValid = decodedToken.id && decodedToken.roleId; const isSeller = decodedToken.roleId === 2; - useEffect(() => { if (tokenIsValid) { isSeller ? navigate("/dashboard") : navigate("/"); @@ -58,7 +56,6 @@ const AppRoutes = () => { return tokenIsValid ? null : children; }; - return ( @@ -117,5 +114,4 @@ const AppRoutes = () => { ); }; - export default AppRoutes; From a4e578d7772442d7284b40104527dd8a000e2804 Mon Sep 17 00:00:00 2001 From: Heisjabo Date: Tue, 30 Jul 2024 14:03:04 +0200 Subject: [PATCH 2/2] fix(chat): users should be able to receive messages in real time --- src/App.tsx | 6 +-- src/__test__/addProducts.test.tsx | 5 ++ src/__test__/productcard.test.tsx | 33 +++++++++++++ src/__test__/userList.test.tsx | 9 ++++ src/components/dashboard/Header.tsx | 4 +- src/components/dashboard/SideBar.tsx | 4 +- src/components/dashboard/Spinner.tsx | 2 +- .../dashboard/admin/AdminSideBar.tsx | 2 +- .../dashboard/products/ProductsTable.tsx | 5 +- src/page-sections/UserList.tsx | 16 ++++++- src/pages/ChatPage.tsx | 48 ++++++++++++------- 11 files changed, 103 insertions(+), 31 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 38ee2d3..4414b75 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -49,11 +49,11 @@ const App: React.FC = () => { {location.pathname !== "/chat" && location.pathname !== "/login" && location.pathname !== "/register" && ( - -
+
(window.location.href = "/chat")}> +
- +
)} ); diff --git a/src/__test__/addProducts.test.tsx b/src/__test__/addProducts.test.tsx index 6624942..34f0d2e 100644 --- a/src/__test__/addProducts.test.tsx +++ b/src/__test__/addProducts.test.tsx @@ -55,6 +55,11 @@ describe("FileUpload component", () => { new File(["dummy content"], "example.png", { type: "image/png" }), ]; + jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useParams: jest.fn(), + })); + beforeEach(() => { (useDropzone as jest.Mock).mockImplementation(() => ({ getRootProps: jest.fn(() => ({ onClick: () => {} })), diff --git a/src/__test__/productcard.test.tsx b/src/__test__/productcard.test.tsx index 6176f53..33ecb36 100644 --- a/src/__test__/productcard.test.tsx +++ b/src/__test__/productcard.test.tsx @@ -77,6 +77,39 @@ describe("ProductCard Component", () => { // expect(addToCartButton).toBeDefined(); }); + test("renders rating and review count correctly", () => { + render( + + + + + , + ); + + const rating = screen.getByTestId("rating"); + const reviewCount = screen.getByTestId("review"); + + expect(rating).toBeDefined(); + expect(reviewCount).toBeDefined(); + }); + + test("truncates long product names", () => { + const longNameProduct = { + ...product, + name: "This is a very long product name that should be truncated", + }; + render( + + + + + , + ); + + const productName = screen.getByTestId("product-name"); + expect(productName.textContent).toHaveLength(15); + }); + test("renders wishlist and view details buttons", () => { render( diff --git a/src/__test__/userList.test.tsx b/src/__test__/userList.test.tsx index 8cbaeb8..d26facb 100644 --- a/src/__test__/userList.test.tsx +++ b/src/__test__/userList.test.tsx @@ -6,8 +6,17 @@ import MockAdapter from "axios-mock-adapter"; import UserList from "../page-sections/UserList"; import chatSlice from "../redux/reducers/chatSlice"; import api from "../redux/api/api"; +import { socket } from "../config/socket"; const mockApi = new MockAdapter(api); + +jest.mock("../config/socket", () => ({ + socket: { + on: jest.fn(), + off: jest.fn(), + }, +})); + const mockStore = (initialState) => configureStore({ reducer: { diff --git a/src/components/dashboard/Header.tsx b/src/components/dashboard/Header.tsx index 1ee19b6..75fcc71 100644 --- a/src/components/dashboard/Header.tsx +++ b/src/components/dashboard/Header.tsx @@ -54,7 +54,7 @@ const Header: React.FC = ({ toggleSidebar }) => { return ( <> -
+
@@ -67,7 +67,7 @@ const Header: React.FC = ({ toggleSidebar }) => {
diff --git a/src/components/dashboard/SideBar.tsx b/src/components/dashboard/SideBar.tsx index 7110c52..df04242 100644 --- a/src/components/dashboard/SideBar.tsx +++ b/src/components/dashboard/SideBar.tsx @@ -26,8 +26,8 @@ const SideBar: React.FC = ({ isOpen }) => { isOpen ? "translate-x-0" : "-translate-x-full" } lg:translate-x-0`} > -
-
+
+
eagles diff --git a/src/components/dashboard/Spinner.tsx b/src/components/dashboard/Spinner.tsx index 940ec9e..2294736 100644 --- a/src/components/dashboard/Spinner.tsx +++ b/src/components/dashboard/Spinner.tsx @@ -1,5 +1,5 @@ const Spinner = () => ( - + = ({ isOpen }) => { isOpen ? "translate-x-0" : "-translate-x-full" } lg:translate-x-0`} > -
+
eagles diff --git a/src/components/dashboard/products/ProductsTable.tsx b/src/components/dashboard/products/ProductsTable.tsx index c5e8492..3941abe 100644 --- a/src/components/dashboard/products/ProductsTable.tsx +++ b/src/components/dashboard/products/ProductsTable.tsx @@ -162,10 +162,7 @@ const ProductsTable: React.FC = () => { ) : products.length === 0 ? ( - + No products found. diff --git a/src/page-sections/UserList.tsx b/src/page-sections/UserList.tsx index 5a4ce15..61314a5 100644 --- a/src/page-sections/UserList.tsx +++ b/src/page-sections/UserList.tsx @@ -8,6 +8,7 @@ import { HiMiniUserGroup } from "react-icons/hi2"; import { useAppDispatch } from "../redux/hooks"; import Spinner from "../components/dashboard/Spinner"; import { fetchChats, fetchUsers } from "../redux/reducers/chatSlice"; +import { socket } from "../config/socket"; interface UserListProps { onUserSelect: (chat: any | null, user: any) => void; @@ -38,6 +39,17 @@ const UserList: React.FC = ({ useEffect(() => { dispatch(fetchChats()); dispatch(fetchUsers()); + + const handlePrivateMessage = () => { + dispatch(fetchChats()); + dispatch(fetchUsers()); + }; + + socket.on("private message recieved", handlePrivateMessage); + + return () => { + socket.off("private message recieved", handlePrivateMessage); + }; }, [dispatch]); useEffect(() => { @@ -114,7 +126,7 @@ const UserList: React.FC = ({ @@ -193,7 +205,7 @@ const UserList: React.FC = ({ {chat.messages.length > 0 ? chat.messages[ chat.messages.length - 1 - ].message.substring(0, 20) + ].message.substring(0, 15) : "No messages yet"} ...

diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 61d5545..1c94329 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useRef } from "react"; import { useSelector } from "react-redux"; import { Link } from "react-router-dom"; -import { MdSportsGolf } from "react-icons/md"; import UserList from "../page-sections/UserList"; import ChatWindow from "../page-sections/ChatWindow"; @@ -31,8 +30,20 @@ const ChatPage: React.FC = () => { setRefetch(true); }; + const handlePastMessages = (pastMessages: any[]) => { + setPublicMessages( + pastMessages.map((msg) => ({ + isOwner: msg.sender === profile?.name, + message: msg.message, + sender: msg.sender, + createdAt: msg.createdAt, + })), + ); + }; + useEffect(() => { dispatch(fetchUser()); + socket.on("past messages", handlePastMessages); }, [dispatch]); useEffect(() => { @@ -43,32 +54,31 @@ const ChatPage: React.FC = () => { }, []); useEffect(() => { - const handleConnect = () => { - socket.emit("private chats"); - }; + socket.on("past messages", handlePastMessages); + }, [isPublicChat]); + useEffect(() => { const handleChatMessage = (msg: any) => { if (!publicMessageIds.current.has(msg.id)) { setPublicMessages((prevMessages) => [...prevMessages, msg]); publicMessageIds.current.add(msg.id); } }; + socket.on("chat message", handleChatMessage); + socket.on("past messages", handlePastMessages); - const handlePastMessages = (pastMessages: any[]) => { - setPublicMessages( - pastMessages.map((msg) => ({ - isOwner: msg.sender === profile?.name, - message: msg.message, - sender: msg.sender, - createdAt: msg.createdAt, - })), - ); + return () => { + socket.off("chat message", handleChatMessage); + socket.off("past messages", handlePastMessages); }; + }, [publicMessageIds, selectedChat]); + useEffect(() => { + const handleConnect = () => { + socket.emit("private chats"); + }; socket.on("connect", handleConnect); - socket.on("chat message", handleChatMessage); - socket.on("past messages", handlePastMessages); - }, [publicMessages]); + }, [selectedChat]); useEffect(() => { const handlePrivateMessage = (message: any) => { @@ -191,6 +201,12 @@ const ChatPage: React.FC = () => { })); try { + socket.emit("private chat message", { + sender: profile?.username, + userId: profile?.id, + receiverId: newChatUser.id, + message: text, + }); await dispatch( sendMessage({ message: text, id: newChatUser.id }), ).unwrap();