Repository: hiteshchoudhary/apihub Branch: main Commit: e3a4414f1810 Files: 548 Total size: 8.7 MB Directory structure: gitextract_cp_asldb/ ├── .babelrc ├── .commitlintrc.json ├── .dockerignore ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.yaml │ ├── code_coverage.yaml │ ├── config.yml │ ├── feature_request.yaml │ └── frontend_contribution.yaml ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── pre-commit ├── .lintstagedrc ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── CONTRIBUTING_CODE_COVERAGE.md ├── CONTRIBUTING_FRONTEND.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── docker-compose.prod.yml ├── docker-compose.yml ├── e2e/ │ ├── common.js │ ├── db.js │ ├── routes/ │ │ ├── apps/ │ │ │ └── todo.test.js │ │ ├── healthcheck.test.js │ │ └── seeds/ │ │ ├── chat-app.test.js │ │ ├── ecommerce.test.js │ │ ├── generated-credentials.test.js │ │ ├── social-media.test.js │ │ └── todo.test.js │ └── test-server.js ├── examples/ │ ├── apps/ │ │ ├── auth/ │ │ │ └── .gitkeep │ │ ├── chat-app/ │ │ │ └── web/ │ │ │ └── react-vite-tailwind/ │ │ │ ├── .eslintrc.cjs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── postcss.config.js │ │ │ ├── src/ │ │ │ │ ├── App.tsx │ │ │ │ ├── api/ │ │ │ │ │ └── index.ts │ │ │ │ ├── components/ │ │ │ │ │ ├── Button.tsx │ │ │ │ │ ├── Input.tsx │ │ │ │ │ ├── Loader.tsx │ │ │ │ │ ├── PrivateRoute.tsx │ │ │ │ │ ├── PublicRoute.tsx │ │ │ │ │ ├── Select.tsx │ │ │ │ │ └── chat/ │ │ │ │ │ ├── AddChatModal.tsx │ │ │ │ │ ├── ChatItem.tsx │ │ │ │ │ ├── GroupChatDetailsModal.tsx │ │ │ │ │ ├── MessageItem.tsx │ │ │ │ │ └── Typing.tsx │ │ │ │ ├── context/ │ │ │ │ │ ├── AuthContext.tsx │ │ │ │ │ └── SocketContext.tsx │ │ │ │ ├── index.css │ │ │ │ ├── interfaces/ │ │ │ │ │ ├── api.ts │ │ │ │ │ ├── chat.ts │ │ │ │ │ └── user.ts │ │ │ │ ├── main.tsx │ │ │ │ ├── pages/ │ │ │ │ │ ├── chat.tsx │ │ │ │ │ ├── login.tsx │ │ │ │ │ └── register.tsx │ │ │ │ ├── utils/ │ │ │ │ │ └── index.ts │ │ │ │ └── vite-env.d.ts │ │ │ ├── tailwind.config.js │ │ │ ├── tsconfig.json │ │ │ ├── tsconfig.node.json │ │ │ └── vite.config.ts │ │ ├── ecommerce/ │ │ │ ├── .gitkeep │ │ │ └── web/ │ │ │ └── react-vite-redux-tailwind/ │ │ │ ├── .eslintrc.cjs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── postcss.config.js │ │ │ ├── public/ │ │ │ │ └── locales/ │ │ │ │ ├── ar/ │ │ │ │ │ └── translation.json │ │ │ │ ├── en/ │ │ │ │ │ └── translation.json │ │ │ │ └── hn/ │ │ │ │ └── translation.json │ │ │ ├── src/ │ │ │ │ ├── App.tsx │ │ │ │ ├── RoutePaths.tsx │ │ │ │ ├── components/ │ │ │ │ │ ├── basic/ │ │ │ │ │ │ ├── ArrowButton.tsx │ │ │ │ │ │ ├── Button.tsx │ │ │ │ │ │ ├── CarouselButtons.tsx │ │ │ │ │ │ ├── Checkbox.tsx │ │ │ │ │ │ ├── DateRangePicker.tsx │ │ │ │ │ │ ├── Drawer.tsx │ │ │ │ │ │ ├── Dropdown.tsx │ │ │ │ │ │ ├── ErrorMessage.tsx │ │ │ │ │ │ ├── FullPageLoadingSpinner.tsx │ │ │ │ │ │ ├── Hamburger.tsx │ │ │ │ │ │ ├── Image.tsx │ │ │ │ │ │ ├── Input.tsx │ │ │ │ │ │ ├── Link.tsx │ │ │ │ │ │ ├── Modal.tsx │ │ │ │ │ │ ├── NavItem.tsx │ │ │ │ │ │ ├── NavList.tsx │ │ │ │ │ │ ├── RadioButtons.tsx │ │ │ │ │ │ ├── RoundedIcon.tsx │ │ │ │ │ │ ├── SearchInput.tsx │ │ │ │ │ │ ├── SelectionMenu.tsx │ │ │ │ │ │ ├── TabItem.tsx │ │ │ │ │ │ ├── Tabs.tsx │ │ │ │ │ │ ├── Text.tsx │ │ │ │ │ │ └── ToastMessage.tsx │ │ │ │ │ ├── business/ │ │ │ │ │ │ ├── AddressCard.tsx │ │ │ │ │ │ ├── AddressCardList.tsx │ │ │ │ │ │ ├── CardContainer.tsx │ │ │ │ │ │ ├── CartItem.tsx │ │ │ │ │ │ ├── CartItemList.tsx │ │ │ │ │ │ ├── CartSummary.tsx │ │ │ │ │ │ ├── CategoryCard.tsx │ │ │ │ │ │ ├── CompanyGurantee.tsx │ │ │ │ │ │ ├── CouponCard.tsx │ │ │ │ │ │ ├── CouponCardList.tsx │ │ │ │ │ │ ├── FooterSection.tsx │ │ │ │ │ │ ├── InvoiceAmountSummary.tsx │ │ │ │ │ │ ├── OrderCard.tsx │ │ │ │ │ │ ├── OrderItem.tsx │ │ │ │ │ │ ├── OrderItemList.tsx │ │ │ │ │ │ ├── OrderListFilters.tsx │ │ │ │ │ │ ├── OrderSummary.tsx │ │ │ │ │ │ ├── OrdersList.tsx │ │ │ │ │ │ ├── Payment.tsx │ │ │ │ │ │ ├── ProductCard.tsx │ │ │ │ │ │ ├── ProductFilters.tsx │ │ │ │ │ │ ├── ProductImagesView.tsx │ │ │ │ │ │ ├── ProductList.tsx │ │ │ │ │ │ ├── QuantityCounter.tsx │ │ │ │ │ │ └── Timer.tsx │ │ │ │ │ ├── icons/ │ │ │ │ │ │ ├── AccountIcon.tsx │ │ │ │ │ │ ├── AddIcon.tsx │ │ │ │ │ │ ├── CartIcon.tsx │ │ │ │ │ │ ├── CloseIcon.tsx │ │ │ │ │ │ ├── DeleteIcon.tsx │ │ │ │ │ │ ├── DownArrow.tsx │ │ │ │ │ │ ├── EditIcon.tsx │ │ │ │ │ │ ├── ErrorIcon.tsx │ │ │ │ │ │ ├── GeneralCategoryIcon.tsx │ │ │ │ │ │ ├── GoogleIcon.tsx │ │ │ │ │ │ ├── GuranteeIcon.tsx │ │ │ │ │ │ ├── HamburgerIcon.tsx │ │ │ │ │ │ ├── HeadphoneIcon.tsx │ │ │ │ │ │ ├── HidePasswordIcon.tsx │ │ │ │ │ │ ├── LeftArrow.tsx │ │ │ │ │ │ ├── LoadingSpinner.tsx │ │ │ │ │ │ ├── LogoutIcon.tsx │ │ │ │ │ │ ├── OrderIcon.tsx │ │ │ │ │ │ ├── RectangleIcon.tsx │ │ │ │ │ │ ├── SearchIcon.tsx │ │ │ │ │ │ ├── ShowPasswordIcon.tsx │ │ │ │ │ │ ├── SubtractIcon.tsx │ │ │ │ │ │ ├── TickIcon.tsx │ │ │ │ │ │ ├── TruckIcon.tsx │ │ │ │ │ │ └── UpArrow.tsx │ │ │ │ │ ├── modals/ │ │ │ │ │ │ ├── addaddressmodal/ │ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ │ └── AddAddressModalContainer.tsx │ │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ │ └── AddAddressModal.tsx │ │ │ │ │ │ ├── changepasswordmodal/ │ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ │ └── ChangePasswordModalContainer.tsx │ │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ │ └── ChangePasswordModal.tsx │ │ │ │ │ │ ├── deleteaddressmodal/ │ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ │ └── DeleteAddressModalContainer.tsx │ │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ │ └── DeleteAddressModal.tsx │ │ │ │ │ │ ├── feedbackmodal/ │ │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ │ └── FeedbackModal.tsx │ │ │ │ │ │ ├── forgotpasswordmodal/ │ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ │ └── ForgotPasswordModalContainer.tsx │ │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ │ └── ForgotPasswordModal.tsx │ │ │ │ │ │ └── logoutmodal/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── LogoutModalContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── LogoutModal.tsx │ │ │ │ │ └── widgets/ │ │ │ │ │ ├── about/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── AboutContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── About.tsx │ │ │ │ │ ├── allproductlist/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── AllProductListContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── AllProductList.tsx │ │ │ │ │ ├── banner/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── BannerContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── Banner.tsx │ │ │ │ │ ├── cart/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── CartContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── Cart.tsx │ │ │ │ │ ├── categorylist/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── CategoryListContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── CategoryList.tsx │ │ │ │ │ ├── checkout/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── CheckoutContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── Checkout.tsx │ │ │ │ │ ├── companyguranteelist/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── CompanyGuranteeListContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── CompanyGuranteeList.tsx │ │ │ │ │ ├── editaddresses/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── EditAddressesContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── EditAddresses.tsx │ │ │ │ │ ├── editprofile/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── EditProfileContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── EditProfile.tsx │ │ │ │ │ ├── exploreproductlist/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── ExploreProductListContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── ExploreProductList.tsx │ │ │ │ │ ├── featuredproductlist/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── FeaturedProductListContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── FeaturedProductList.tsx │ │ │ │ │ ├── footer/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── FooterContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── Footer.tsx │ │ │ │ │ ├── header/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── HeaderContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── Header.tsx │ │ │ │ │ ├── infoheader/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── InfoHeaderContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── InfoHeader.tsx │ │ │ │ │ ├── login/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── LoginContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── Login.tsx │ │ │ │ │ ├── myaccountoption/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── MyAccountOptionContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── MyAccountOption.tsx │ │ │ │ │ ├── myorderslist/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── MyOrdersListContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── MyOrdersList.tsx │ │ │ │ │ ├── orderdetail/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── OrderDetailContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── OrderDetail.tsx │ │ │ │ │ ├── productdetails/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── ProductDetailsContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── ProductDetails.tsx │ │ │ │ │ ├── relateditemslist/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── RelatedItemsListContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── RelatedItemsList.tsx │ │ │ │ │ ├── resetforgottenpassword/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── ResetForgottenPasswordContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── ResetForgottenPassword.tsx │ │ │ │ │ └── signup/ │ │ │ │ │ ├── container/ │ │ │ │ │ │ └── SignupContainer.tsx │ │ │ │ │ └── presentation/ │ │ │ │ │ └── Signup.tsx │ │ │ │ ├── constants.ts │ │ │ │ ├── data/ │ │ │ │ │ └── applicationData.tsx │ │ │ │ ├── hooks/ │ │ │ │ │ ├── useBreakpointCheck.tsx │ │ │ │ │ ├── useCustomNavigate.tsx │ │ │ │ │ ├── useOnRefresh.tsx │ │ │ │ │ └── useOutsideClick.tsx │ │ │ │ ├── i18n.ts │ │ │ │ ├── index.css │ │ │ │ ├── layouts/ │ │ │ │ │ └── PageLayout.tsx │ │ │ │ ├── main.tsx │ │ │ │ ├── pages/ │ │ │ │ │ ├── about/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── AboutPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── AboutPage.tsx │ │ │ │ │ ├── cart/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── CartPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── CartPage.tsx │ │ │ │ │ ├── checkout/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── CheckoutPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── CheckoutPage.tsx │ │ │ │ │ ├── home/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── HomePageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── HomePage.tsx │ │ │ │ │ ├── login/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── LoginPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── LoginPage.tsx │ │ │ │ │ ├── manageaccount/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── ManageAccountPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── ManageAccountPage.tsx │ │ │ │ │ ├── orderdetail/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── OrderDetailPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── OrderDetailPage.tsx │ │ │ │ │ ├── orders/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── OrdersPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── OrdersPage.tsx │ │ │ │ │ ├── pagenotfound/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── PageNotFoundPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── PageNotFoundPage.tsx │ │ │ │ │ ├── paymentfeedback/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── PaymentFeedbackPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── PaymentFeedbackPage.tsx │ │ │ │ │ ├── productdetail/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── ProductDetailPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── ProductDetailPage.tsx │ │ │ │ │ ├── products/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── ProductsPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── ProductsPage.tsx │ │ │ │ │ ├── productsearch/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── ProductSearchPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── ProductSearchPage.tsx │ │ │ │ │ ├── resetforgottenpassword/ │ │ │ │ │ │ ├── container/ │ │ │ │ │ │ │ └── ResetForgottenPasswordPageContainer.tsx │ │ │ │ │ │ └── presentation/ │ │ │ │ │ │ └── ResetForgottenPasswordPage.tsx │ │ │ │ │ └── signup/ │ │ │ │ │ ├── container/ │ │ │ │ │ │ └── SignupPageContainer.tsx │ │ │ │ │ └── presentation/ │ │ │ │ │ └── SignupPage.tsx │ │ │ │ ├── protectedroutes/ │ │ │ │ │ └── ForLoggedInUsers.tsx │ │ │ │ ├── services/ │ │ │ │ │ ├── ApiError.ts │ │ │ │ │ ├── ApiRequest.ts │ │ │ │ │ ├── ApiResponse.ts │ │ │ │ │ ├── CountryApiRequest.ts │ │ │ │ │ ├── address/ │ │ │ │ │ │ ├── AddressService.ts │ │ │ │ │ │ └── AddressTypes.ts │ │ │ │ │ ├── auth/ │ │ │ │ │ │ ├── AuthService.ts │ │ │ │ │ │ └── AuthTypes.ts │ │ │ │ │ ├── cart/ │ │ │ │ │ │ ├── CartService.ts │ │ │ │ │ │ └── CartTypes.ts │ │ │ │ │ ├── category/ │ │ │ │ │ │ ├── CategoryService.ts │ │ │ │ │ │ └── CategoryTypes.ts │ │ │ │ │ ├── countryapi/ │ │ │ │ │ │ ├── CountryApiService.ts │ │ │ │ │ │ └── CountryApiTypes.ts │ │ │ │ │ ├── coupon/ │ │ │ │ │ │ ├── CouponService.ts │ │ │ │ │ │ └── CouponTypes.ts │ │ │ │ │ ├── order/ │ │ │ │ │ │ ├── OrderService.ts │ │ │ │ │ │ └── OrderTypes.ts │ │ │ │ │ ├── product/ │ │ │ │ │ │ ├── ProductService.ts │ │ │ │ │ │ └── ProductTypes.ts │ │ │ │ │ └── profile/ │ │ │ │ │ ├── ProfileService.ts │ │ │ │ │ └── ProfileTypes.ts │ │ │ │ ├── store/ │ │ │ │ │ ├── AuthSlice.ts │ │ │ │ │ ├── BreakpointSlice.ts │ │ │ │ │ ├── CartSlice.ts │ │ │ │ │ ├── LanguageSlice.ts │ │ │ │ │ ├── ToastMessageSlice.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── styles/ │ │ │ │ │ └── DateRangePicker.css │ │ │ │ ├── utils/ │ │ │ │ │ ├── asyncHandler.ts │ │ │ │ │ ├── breakpointsHelper.ts │ │ │ │ │ ├── commonHelper.ts │ │ │ │ │ ├── countryApiAsyncHandler.ts │ │ │ │ │ ├── dateTimeHelper.ts │ │ │ │ │ └── languageHelpers.ts │ │ │ │ └── vite-env.d.ts │ │ │ ├── tailwind.config.js │ │ │ ├── tsconfig.json │ │ │ ├── tsconfig.node.json │ │ │ └── vite.config.ts │ │ ├── social-media/ │ │ │ └── .gitkeep │ │ └── todo/ │ │ └── web/ │ │ └── react-vite-tailwind/ │ │ ├── .eslintrc.cjs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── api/ │ │ │ │ └── index.ts │ │ │ ├── components/ │ │ │ │ ├── Button.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── Input.tsx │ │ │ │ ├── Loader.tsx │ │ │ │ ├── ModalContainer.tsx │ │ │ │ ├── Options.tsx │ │ │ │ └── todos/ │ │ │ │ ├── CreateTodoModal.tsx │ │ │ │ ├── DetailAndEditModal.tsx │ │ │ │ ├── TabsHeader.tsx │ │ │ │ └── TodoCard.tsx │ │ │ ├── context/ │ │ │ │ └── TodoContext.tsx │ │ │ ├── index.css │ │ │ ├── interfaces/ │ │ │ │ ├── api.ts │ │ │ │ └── todo.ts │ │ │ ├── main.tsx │ │ │ ├── utils/ │ │ │ │ └── index.ts │ │ │ └── vite-env.d.ts │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── kitchen-sink/ │ │ ├── cookies/ │ │ │ └── .gitkeep │ │ ├── httpmethods/ │ │ │ └── .gitkeep │ │ ├── images/ │ │ │ └── .gitkeep │ │ ├── redirects/ │ │ │ └── .gitkeep │ │ ├── requestinspections/ │ │ │ └── .gitkeep │ │ ├── responseinspections/ │ │ │ └── .gitkeep │ │ └── statuscodes/ │ │ └── web/ │ │ └── react-shadcn-tailwind-zustand/ │ │ ├── .eslintrc.cjs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── components.json │ │ ├── index.html │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── components/ │ │ │ │ └── ui/ │ │ │ │ ├── accordion.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── command.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── navigation-menu.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── radio-group.tsx │ │ │ │ └── select.tsx │ │ │ ├── constants/ │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── index.css │ │ │ ├── layout/ │ │ │ │ ├── header/ │ │ │ │ │ ├── Header.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── menuItem.ts │ │ │ │ ├── index.ts │ │ │ │ └── pageContainer/ │ │ │ │ ├── PageContainer.tsx │ │ │ │ └── index.ts │ │ │ ├── lib/ │ │ │ │ └── utils.ts │ │ │ ├── main.tsx │ │ │ ├── pages/ │ │ │ │ ├── codesList/ │ │ │ │ │ ├── CodesList.tsx │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── StatusAccordian.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── types.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── findCode/ │ │ │ │ │ ├── FindCode.tsx │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── ComboBox.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── home/ │ │ │ │ │ ├── Home.tsx │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── home.css │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── quiz/ │ │ │ │ ├── Quiz.tsx │ │ │ │ ├── components/ │ │ │ │ │ ├── QuestionsCard.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── router/ │ │ │ │ ├── index.ts │ │ │ │ ├── router.tsx │ │ │ │ └── routes.ts │ │ │ ├── services/ │ │ │ │ ├── codesList.ts │ │ │ │ └── types.ts │ │ │ ├── store/ │ │ │ │ └── store.ts │ │ │ └── vite-env.d.ts │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ └── public/ │ ├── books/ │ │ └── .gitkeep │ ├── cats/ │ │ └── .gitkeep │ ├── dogs/ │ │ └── .gitkeep │ ├── meals/ │ │ └── .gitkeep │ ├── quotes/ │ │ └── .gitkeep │ ├── randomjokes/ │ │ └── .gitkeep │ ├── randomproducts/ │ │ └── .gitkeep │ ├── randomusers/ │ │ └── .gitkeep │ ├── stocks/ │ │ └── .gitkeep │ └── youtube/ │ └── .gitkeep ├── nodemon.json ├── package.json ├── playwright.config.js ├── prepare.js ├── public/ │ ├── assets/ │ │ └── templates/ │ │ ├── html_response.html │ │ └── xml_response.xml │ ├── images/ │ │ └── .gitkeep │ └── temp/ │ └── .gitkeep └── src/ ├── app.js ├── constants.js ├── controllers/ │ ├── apps/ │ │ ├── auth/ │ │ │ └── user.controllers.js │ │ ├── chat-app/ │ │ │ ├── chat.controllers.js │ │ │ └── message.controllers.js │ │ ├── ecommerce/ │ │ │ ├── address.controllers.js │ │ │ ├── cart.controllers.js │ │ │ ├── category.controllers.js │ │ │ ├── coupon.controllers.js │ │ │ ├── order.controllers.js │ │ │ ├── product.controllers.js │ │ │ └── profile.controllers.js │ │ ├── social-media/ │ │ │ ├── bookmark.controllers.js │ │ │ ├── comment.controllers.js │ │ │ ├── follow.controllers.js │ │ │ ├── like.controllers.js │ │ │ ├── post.controllers.js │ │ │ └── profile.controllers.js │ │ └── todo/ │ │ └── todo.controllers.js │ ├── healthcheck.controllers.js │ ├── kitchen-sink/ │ │ ├── cookie.controllers.js │ │ ├── httpmethod.controllers.js │ │ ├── image.controllers.js │ │ ├── redirect.controllers.js │ │ ├── requestinspection.controllers.js │ │ ├── responseinspection.controllers.js │ │ └── statuscode.controllers.js │ └── public/ │ ├── book.controllers.js │ ├── cat.controllers.js │ ├── dog.controllers.js │ ├── meal.controllers.js │ ├── quote.controllers.js │ ├── randomjoke.controllers.js │ ├── randomproduct.controllers.js │ ├── randomuser.controllers.js │ ├── stock.controllers.js │ └── youtube.controllers.js ├── db/ │ └── index.js ├── index.js ├── json/ │ ├── books.json │ ├── cats.json │ ├── dogs.json │ ├── meals.json │ ├── nse-stocks.json │ ├── quotes.json │ ├── randomjoke.json │ ├── randomproduct.json │ ├── randomuser.json │ ├── status-codes.json │ └── youtube/ │ ├── channel.json │ ├── comments.json │ ├── playlistitems.json │ ├── playlists.json │ └── videos.json ├── logger/ │ ├── morgan.logger.js │ └── winston.logger.js ├── middlewares/ │ ├── auth.middlewares.js │ ├── error.middlewares.js │ └── multer.middlewares.js ├── models/ │ └── apps/ │ ├── auth/ │ │ └── user.models.js │ ├── chat-app/ │ │ ├── chat.models.js │ │ └── message.models.js │ ├── ecommerce/ │ │ ├── address.models.js │ │ ├── cart.models.js │ │ ├── category.models.js │ │ ├── coupon.models.js │ │ ├── order.models.js │ │ ├── product.models.js │ │ └── profile.models.js │ ├── social-media/ │ │ ├── bookmark.models.js │ │ ├── comment.models.js │ │ ├── follow.models.js │ │ ├── like.models.js │ │ ├── post.models.js │ │ └── profile.models.js │ └── todo/ │ └── todo.models.js ├── passport/ │ └── index.js ├── routes/ │ ├── apps/ │ │ ├── auth/ │ │ │ └── user.routes.js │ │ ├── chat-app/ │ │ │ ├── chat.routes.js │ │ │ └── message.routes.js │ │ ├── ecommerce/ │ │ │ ├── address.routes.js │ │ │ ├── cart.routes.js │ │ │ ├── category.routes.js │ │ │ ├── coupon.routes.js │ │ │ ├── order.routes.js │ │ │ ├── product.routes.js │ │ │ └── profile.routes.js │ │ ├── social-media/ │ │ │ ├── bookmark.routes.js │ │ │ ├── comment.routes.js │ │ │ ├── follow.routes.js │ │ │ ├── like.routes.js │ │ │ ├── post.routes.js │ │ │ └── profile.routes.js │ │ └── todo/ │ │ └── todo.routes.js │ ├── healthcheck.routes.js │ ├── kitchen-sink/ │ │ ├── cookie.routes.js │ │ ├── httpmethod.routes.js │ │ ├── image.routes.js │ │ ├── redirect.routes.js │ │ ├── requestinspection.routes.js │ │ ├── responseinspection.routes.js │ │ └── statuscode.routes.js │ └── public/ │ ├── book.routes.js │ ├── cat.routes.js │ ├── dog.routes.js │ ├── meal.routes.js │ ├── quote.routes.js │ ├── randomjoke.routes.js │ ├── randomproduct.routes.js │ ├── randomuser.routes.js │ ├── stock.routes.js │ └── youtube.routes.js ├── seeds/ │ ├── _constants.js │ ├── chat-app.seeds.js │ ├── ecommerce.seeds.js │ ├── social-media.seeds.js │ ├── todo.seeds.js │ └── user.seeds.js ├── socket/ │ └── index.js ├── swagger.yaml ├── utils/ │ ├── ApiError.js │ ├── ApiResponse.js │ ├── asyncHandler.js │ ├── helpers.js │ └── mail.js └── validators/ ├── apps/ │ ├── auth/ │ │ └── user.validators.js │ ├── chat-app/ │ │ ├── chat.validators.js │ │ └── message.validators.js │ ├── ecommerce/ │ │ ├── address.validators.js │ │ ├── cart.validators.js │ │ ├── category.validators.js │ │ ├── coupon.validators.js │ │ ├── order.validators.js │ │ ├── product.validators.js │ │ └── profile.validators.js │ ├── social-media/ │ │ ├── comment.validators.js │ │ ├── post.validators.js │ │ └── profile.validators.js │ └── todo/ │ └── todo.validators.js ├── common/ │ └── mongodb.validators.js ├── kitchen-sink/ │ ├── cookie.validators.js │ ├── redirect.validators.js │ ├── responseinspection.validators.js │ └── statuscode.validators.js └── validate.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["@babel/preset-env"], "plugins": ["@babel/plugin-syntax-import-assertions"] } ================================================ FILE: .commitlintrc.json ================================================ { "extends": ["@commitlint/config-conventional"], "rules": { "type-enum": [ 2, "always", [ "ci", "chore", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "assets", "test" ] ] } } ================================================ FILE: .dockerignore ================================================ /.vscode /node_modules ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: 🐞 Bug report description: Create a report to help us improve title: "BUG: " labels: - bug body: - type: textarea id: bug_description attributes: label: Describe the bug description: A clear and concise description of what the bug is. validations: required: true - type: textarea id: reproduce_steps attributes: label: To Reproduce description: Steps to reproduce the behavior. validations: required: false - type: textarea id: expected_behavior attributes: label: Expected behavior description: A clear and concise description of what you expected to happen. validations: required: false - type: textarea id: screenshots attributes: label: Screenshots description: If applicable, add screenshots to help explain your problem. - type: input id: os_info attributes: label: OS description: Operating System information (e.g., iOS). placeholder: MacOS validations: required: false - type: input id: version_info attributes: label: OS Version description: OS Version information. placeholder: "13.1" validations: required: false - type: input id: client_info attributes: label: Client description: Client information (e.g., postman, thunder client, chrome, safari). placeholder: Postman validations: required: false - type: textarea id: additional_context attributes: label: Additional context or Information description: Add any other context or any other information about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/code_coverage.yaml ================================================ name: 🧪 Testing Contribution description: Share details about your test coverage for contribution to FreeAPI. title: "TESTING: <title>" labels: - testing body: - type: textarea id: test_description attributes: label: Describe the test description: A clear and concise description of the proposed testing suite. validations: required: true - type: textarea id: additional_info attributes: label: Additional Information description: Add any extra details or considerations that might be relevant such as screenshots. validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: FreeAPI Discord Server url: https://hitesh.ai/discord about: Please ask and answer questions related to FreeAPI here. - name: Learn FreeAPI here url: https://www.youtube.com/playlist?list=PLRAV69dS1uWSx4erHGq8hW_GE-Eaj60r- about: Gain comprehensive knowledge of FreeAPI through this curated playlist. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yaml ================================================ name: 🌟 Feature Request description: Suggest an enhancement, new feature or anything title: "FEATURE: <title>" labels: - enhancement body: - type: textarea id: feature_description attributes: label: Describe the Feature description: A clear and concise description of the proposed feature or enhancement. validations: required: true - type: textarea id: use_case attributes: label: Use Case description: Provide a scenario or use case where this feature would be beneficial. validations: required: false - type: textarea id: additional_info attributes: label: Additional Information description: Add any extra details or considerations that might be relevant. validations: required: false - type: textarea id: suggested_tools attributes: label: Suggested Tools description: List any tools or technologies you suggest for implementing this feature. validations: required: false - type: textarea id: additional_context attributes: label: Additional Context or Information description: Add any other context or information that might be useful for understanding the feature request. ================================================ FILE: .github/ISSUE_TEMPLATE/frontend_contribution.yaml ================================================ name: 🚀 Frontend Contribution description: Share details about your frontend application for contribution to FreeAPI. title: "FRONTEND: <title>" labels: - frontend - enhancement body: - type: input id: app_title attributes: label: Frontend App Title description: Provide a title for your frontend application. validations: required: true - type: textarea id: overview attributes: label: Overview description: Briefly describe your frontend application. validations: required: true - type: textarea id: detailed_features attributes: label: Detailed Features description: List the features of your frontend application in detail. validations: required: true - type: input id: tech_stack attributes: label: Tech Stack Used description: Specify the technologies used in your frontend application (comma-separated). placeholder: "ReactJs, redux, tailwindCSS..." validations: required: true - type: input id: platform attributes: label: Built For Platform description: Specify the platform on which this application will run. placeholder: "web, mobile, desktop..." validations: required: true - type: input id: project_path attributes: label: Project Path description: Provide the path to your project within the FreeAPI project's `examples` folder. placeholder: E.g. $ROOT_FOLDER/examples/apps/social-media/web/react-redux-tailwind/<your_project_folders> validations: required: true ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # Logs logs /tmp /.vscode /.idea # ignore the image files uploaded in the public/images folder /public/images/* /public/temp/* # except .gitkeep to push empty folder to the git repo !/public/images/.gitkeep !/public/temp/.gitkeep NOTES.md .DS_Store # Playwright /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ ================================================ FILE: .husky/commit-msg ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx --no-install commitlint --edit ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx lint-staged --allow-empty ================================================ FILE: .lintstagedrc ================================================ { "**/*.{js,json}": ["prettier --write"] } ================================================ FILE: .prettierignore ================================================ /.vscode /node_modules ./dist *.yml *.yaml *.env .env .env.* ================================================ FILE: .prettierrc ================================================ { "singleQuote": false, "bracketSpacing": true, "tabWidth": 2, "trailingComma": "es5", "semi": true } ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to FreeAPI Thank you for your interest in contributing to FreeAPI! We welcome contributions from the software community to help improve and enhance this project. By contributing, you can help make FreeAPI a more valuable resource for developers and contribute to the growth of the open-source community. ## Table of Contents 1. [📙 How to Contribute](#how-to-contribute) 2. [䷫ Commit Message Format](#commit-message-format) 3. [👨🏻‍⚖️ Guidelines for Contribution](#contribution-guidelines) 4. [🚀 Contribute as a frontend developer](#frontend-contributor) ## How to Contribute <a name="how-to-contribute"></a> To contribute to FreeAPI, please follow these guidelines: 1. Fork the repository on GitHub. 2. Clone your forked repository to your local machine. 3. Create a new branch for your feature or bug fix: `git checkout -b feat/your-feature-name` or `git checkout -b fix/your-bug-fix-name` and make your changes. 4. Run all the tests 🧪 before committing the changes and make sure all tests are passed. 5. After all tests are passed, commit your changes with a descriptive messages: `git commit -am 'add your commit message'`. 6. Push your changes to your forked repository: `git push origin feat/your-feature-name`. 7. Submit a pull request to the main repository. ## Commit Message Format <a name="commit-message-format"></a> We follow the conventional commit message format to provide a clear and standardized history of our project's changes. Each commit message should consist of a type and a descriptive message. | Type | Heading | Rule | Description | | -------- | -------- | ------------------------------------------ | --------------------------------------------------------------------------------- | | ci | CI | Continuous Integration | Changes related to continuous integration. | | chore | Chore | Maintenance tasks | Other changes that don't affect production. | | docs | Docs | Documentation | Changes related to documentation. | | feat | Feature | New Feature | New feature implementations or additions. | | fix | Fix | Bug Fixes | Bug fixes or corrections. | | perf | Perf | Performance Improvements | Performance-related improvements. | | refactor | Refactor | Code Refactoring | Code changes that don't fix bugs or add features, but improve the code structure. | | revert | Revert | Revert Previous Commits | Reverting previous commits. | | style | Style | Code Formatting or Style | Changes related to code formatting or style. | | assets | Assets | Add or Update Assets (e.g., images, files) | Changes related to adding or updating assets, such as images or other files. | ### Format The commit message should start with the type, followed by a colon and a space, and then the descriptive message in present tense. Example: - feat: add user authentication feature - fix: correct typo in README Please adhere to this format when making commits. This will help us maintain a clean and organized commit history. ## Guidelines for Contribution <a name="contribution-guidelines"></a> Here's a guide on how you can effectively contribute to our API hub: 1. Pull Requests for Readme Updates: Please refrain from sending pull requests solely for updating the project's readme file. While we appreciate the importance of clear and concise documentation, we prefer to focus on substantial code contributions and feature enhancements. 2. Grammar Updates: Our team values effective communication, but we're not grammar sticklers. You don't need to send pull requests solely for grammar fixes or minor language improvements. Instead, concentrate on the core functionalities and features of the project. 3. Avoid Updating Existing Public APIs: To maintain stability and consistency, we discourage direct updates to existing public APIs within the API hub. These APIs have been thoroughly tested and approved. However, if you encounter any bugs or issues, we encourage you to open an issue on our project's issue tracker to notify us. 4. Build New Project APIs: We encourage you to explore your creativity and contribute by building complete project APIs. These APIs should provide comprehensive solutions that can assist developers in constructing complex projects to showcase their skills and abilities. Your contributions in this area will greatly benefit the community. 5. Draft a Proposal and Discuss on Discord: Before diving into your project, we recommend drafting a proposal. This can include a mind map or outline of the API you intend to build and its potential benefits. Join our Discord community, where you can share your proposal, discuss ideas, and gather feedback from fellow contributors. Engaging in these discussions will enhance your backend portfolio and help shape the future direction of the project. We appreciate your enthusiasm and look forward to your valuable contributions to our open source API hub project. Together, we can foster a collaborative environment and make a significant impact in the API integration landscape. ## Contribute as a frontend developer <a name="frontend-contributor"></a> Are you a Frontend developer looking to create elegant mobile/web apps which consumes FreeAPIs? Then follow this [Frontend Contribution guide](https://github.com/hiteshchoudhary/apihub/blob/main/CONTRIBUTING_FRONTEND.md) and contribute as a frontend developer ================================================ FILE: CONTRIBUTING_CODE_COVERAGE.md ================================================ # FreeAPI Testing Contribution Guide Thank you for your interest in contributing to the FreeAPI project to help us deliver our APIs that are battlefield tested. To ensure reliability & stability for our end users, we utilize Playwright, a powerful testing framework to automate testing across all endpoints. ### Why Playwright? After exploring our open-source community options such as Jest, Jasmine & Playwright. We decided to move forward with the playwright as it facilitates automated testing, offers cross-browser support, rich enough API with familiar syntax. ## ⚠️ Important Note: ### Before starting your contribution - create an issue stating which module you will be working on - in a given module if tests exist, we do not welcome them as long as it is a logical fix - test coverage for example frontend apps are not our top priority **IMPORTANT: Contributor must create an issue with [Testing Contribution](https://github.com/hiteshchoudhary/apihub/issues/new?assignees=&labels=testing&projects=&template=code_coverage.yaml&title=TESTING%3A+%3Ctitle%3E) issue template.** This ensures coordination and prevents duplicated efforts. ## Table of Contents 1. [🏁 Getting Started](#getting-started) 2. [👆🏻 Choosing a Module](#choosing-a-module) 3. [🗂️ Folder Structure](#folder-structure-main) 4. [📙 Coding Standards](#coding-standards) 5. [📝 Dependency Management](#dependency-management) 6. [📨 Submitting Your Contribution](#submitting-your-contribution) ## Getting Started <a name="getting-started"></a> ### Fork the Repository Start by forking the FreeAPI project repository to your GitHub account. ### Clone Your Fork Clone your fork of the repository to your local machine. ```bash git clone https://github.com/<your_username>/apihub.git cd apihub ``` ### Install Dependencies Make sure you have the necessary dependencies of FreeAPI installed for the frontend framework or library you plan to use. Follow this [README.md section](https://github.com/hiteshchoudhary/apihub/blob/main/README.md#-installation) to know more about setting up the FreeAPI environment ## Choosing a Module <a name="choosing-a-module"></a> Decide which module you want to contribute to. Browse the `/apps`, `/public`, or `/kitchen-sink` modules to explore the available modules and APIs. Read the following section carefully to understand the folder structure you need to follow to increase your chances of getting your PR approved. ## Folder Structure <a name="folder-structure-main"></a> Follow the specified folder structure for your frontend application (**The folder names must not follow the camel casing to keep things consistent.** _Your actual project code folders may have camel casing_): ``` ROOT_FOLDER/e2e/{module}/ ``` See the following examples with context for the above structure: ## Example: Todo endpoint testing Imagine you want to test the todo endpoint that is part of `/apps` module of the FreeAPI project. To keep a consistent folder structure for backtracking name your files with name identifier, but with an extension of `.test.js`. ### Folder Structure: ``` $ROOT_FOLDER/e2e/{package}/{module}/{file-indicator}.test.js ``` ## Explanation for the examples: - `ROOT_FOLDER`: Refers to the root directory of the FreeAPI project. - `e2e`: This directory is designated for including test cases. - `{package}`: The directory contains typical of a Node.js application: `app.js` initializes the app, `controllers` handle requests, `models` define data structures, `routes` map endpoints, `utils` provide utilities, and others manage logging, authentication, and database interactions. - `{module}`: `apps` or `public` or `kitchen-sink`: Denotes the chosen module (`/apps` for complex apps, `/public` for public APIs, `/kitchen-sink` for backend-related static APIs. These folders are already created officially). - `{file-indicator}`: This is the actual file name indicator that helps to identify for which file you are writing test cases for example: `todo.test.js` By following this standardized folder structure, contributors can easily organize their front-end projects, making it convenient for others to explore, understand, and replicate the implementation. ## Coding Standards <a name="coding-standards"></a> Adhere to the coding standards of the playwright framework. Additionally, consider the following guidelines: - Use clear descriptive test suite & block names - Follow best practices for unit, integration & end-to-end testing - Ensure your code is well-documented by comments wherever necessary - Make sure to add both positive & negative test cases ## Dependency Management <a name="dependency-management"></a> Ideally, we do not encourage you to include a new package. Confine your code practices within the available dependencies to avoid overheads. Please state with a clear explanation with examples if you add anything new. ## Submitting Your Contribution <a name="submitting-your-contribution"></a> Click [here](https://github.com/hiteshchoudhary/apihub/blob/main/CONTRIBUTING.md) for a detailed contribution guide on submitting a PR. Thank you for your contribution to FreeAPI! Your dedication helps make our APIs more accessible and valuable to the community. If you have any questions or need assistance, feel free to reach out to our [Discord](https://hitesh.ai/discord). Thank you for being part of the FreeAPI community and contributing to the project! We look forward to featuring your fantastic work. 🌟 Happy coding! 🚀 ================================================ FILE: CONTRIBUTING_FRONTEND.md ================================================ # FreeAPI Frontend Contribution Guide Thank you for your interest in contributing to the FreeAPI project by creating frontend applications! Your efforts play a crucial role in enhancing the user experience and expanding the reach of our APIs. Please follow this guide to ensure a smooth and collaborative contribution process. ## ⚠️ Important Note: ### Before starting your contribution - create an issue stating the app you will be working on - the technology stack you plan to use - the platform you intend to target. - describe your application in detail **IMPORTANT: Contributor must create an issue with [Frontend Contribution](https://github.com/hiteshchoudhary/apihub/issues/new?assignees=&labels=frontend%2Cenhancement&projects=&template=frontend_contribution.yaml&title=FRONTEND%3A+%3Ctitle%3E) issue template.** This ensures coordination and prevents duplicated efforts. ## Table of Contents 1. [🏁 Getting Started](#getting-started) 2. [👆🏻 Choosing a Module](#choosing-a-module) 3. [🗂️ Folder Structure](#folder-structure-main) 4. [📙 Coding Standards](#coding-standards) 5. [📝 Dependency Management](#dependency-management) 6. [🧪 Testing (optional)](#testing) 7. [📨 Submitting Your Contribution](#submitting-your-contribution) 8. [🌟 Featured Projects Opportunity on FreeAPI](#featured-projects) 9. [🌟 How To Get Featured](#get-featured) ## Getting Started <a name="getting-started"></a> ### Fork the Repository Start by forking the FreeAPI project repository to your GitHub account. ### Clone Your Fork Clone your fork of the repository to your local machine. ```bash git clone https://github.com/<your_username>/apihub.git cd apihub ``` ### Install Dependencies Make sure you have the necessary dependencies of FreeAPI installed for the frontend framework or library you plan to use. Follow this [README.md section](https://github.com/hiteshchoudhary/apihub/blob/main/README.md#-installation) to know more about setting up the FreeAPI environment ## Choosing a Module <a name="choosing-a-module"></a> Decide which module you want to contribute to. Browse the `/apps`, `/public`, or `/kitchen-sink` modules to explore the available modules and APIs. Read the following section carefully to understand the folder structure you need to follow to increase chances to get your PR approved. ## Folder Structure <a name="folder-structure-main"></a> Follow the specified folder structure for your frontend application (**The folder names must not follow the camel casing to keep things consistent.** _Your actual project code folders may have camel casing_): ``` ROOT_FOLDER/examples/{module}/{app-name}/{platform}/{frontend-tech-used}/{project-code} ``` See the following examples with context for above structure: ## Example 1: Social Media Web App Imagine you want to create a web application for the `social-media` project within the `/apps` module of the FreeAPI project. You've decided to use React.js for the frontend, manage state with Redux, and style the app with Tailwind CSS. Your project folder name will be a unique identifier for your application because we are not allowing same application to be built using same tech stack. ### Folder Structure: ``` $ROOT_FOLDER/examples/apps/social-media/web/react-redux-tailwind/<your_project_folders> ``` ## Example 2: YouTube Mobile App Suppose you are interested in building a mobile application that consumes the YouTube API from the `/public` folder of the FreeAPI project. For this, you've chosen Flutter as your framework, and you'll be using Riverpod for state management. Again, your project folder name will be a unique identifier for your application because we are not allowing same application to be built using same tech stack. ### Folder Structure: ```bash $ROOT_FOLDER/examples/public/youtube/mobile/flutter-riverpod/<your_project_folders> ``` ## Example 3: Status Code Lookup App You want to contribute a frontend application to the `/kitchen-sink` module that allows users to input a numerical HTTP status code. The app will then provide details about that status code, such as its purpose and common usage with elegant UI. ## Folder Structure: ```bash $ROOT_FOLDER/examples/kitchen-sink/statuscodes/web/react-tailwind/<your_project_folders> ``` ## Explanation for the examples: - `ROOT_FOLDER:` Refers to the root directory of the FreeAPI project. - `examples:` This directory is designated for frontend examples. - `{module} - apps or public or kitchen-sink:` Denotes the chosen module (`/apps` for complex apps, `/public` for public APIs, `/kitchen-sink` for backend-related static APIs. These folders are already created officially). - `{app_name} - social-media or youtube or statuscode:` Specifies the name of the chosen app or API within the selected module. This folder is also created by default officially. - `{platform} - web or mobile or desktop:` Defines the platform for which the frontend is developed (web or mobile or desktop etc). - `{frontend-tech-used} - react-redux-tailwind or flutter-provider:` Indicates the frontend technology stack used, including the framework, UI-lib and state management tool (at least one tech tool name must be there). - `<your_project_folders>:` Represents the actual project folders that will be coding. By following this standardized folder structure, contributors can easily organize their frontend projects, making it convenient for others to explore, understand, and replicate the implementation. ## Coding Standards <a name="coding-standards"></a> Adhere to the coding standards of the chosen frontend technology and framework. Additionally, consider the following guidelines: - Use clear and descriptive variable and function names. - Follow best practices for state management, component structure, and code organization. - Ensure your code is well-documented by comments wherever necessary. - Try to use type safe languages like TypeScript over JavaScript to code the frontend - **Include comprehensive README.md file for each project on how to do installation and setup for the respective apps** ## Dependency Management <a name="dependency-management"></a> List all major dependencies/tech tools used in a clear and organized manner in your project README.md. Include version numbers to ensure compatibility. ## Testing (optional) <a name="testing"></a> Write tests for your frontend application to ensure its functionality. Include instructions on how to run the tests. ## Submitting Your Contribution <a name="submitting-your-contribution"></a> Click [here](https://github.com/hiteshchoudhary/apihub/blob/main/CONTRIBUTING.md) for detailed contribution guide on submitting a PR. Thank you for your contribution to FreeAPI! Your dedication helps make our APIs more accessible and valuable to the community. If you have any questions or need assistance, feel free to reach out to our [Discord](https://hitesh.ai/discord). ## 🌟 Featured Projects Opportunity on FreeAPI <a name="featured-projects"></a> We value the contributions of our community members, and we want to showcase your work! As part of our effort to highlight the creativity and diversity of the FreeAPI project, we are introducing a Featured Projects section on the official [FreeAPI](https://freeapi.app) site (under development). ## How to Get Featured <a name="get-featured"></a> 1. **Self-Host the FreeAPI Backend:** - Ensure that you have set up and are self-hosting the FreeAPI backend on your server. Detailed instructions can be found in the [README.md Railway one click deploy section](https://github.com/hiteshchoudhary/apihub/blob/main/README.md#-using-railway-one-click-deploy). _(NOTE: You can deploy this app on server that you are comfortable with. Just make sure it is deployed and your deployed frontend can consume it's apis)_ 2. **Consume FreeAPI Backend APIs in Your Frontend:** - In your frontend project, make sure you are consuming the relevant FreeAPI backend APIs from the deployed backend server. This demonstrates the end-to-end functionality of your application. 3. **Deploy Your Frontend:** - Deploy your frontend application, and ensure it is accessible via a public URL. 4. **Include Deployed Link in README.md:** - Add the deployed link to your frontend application at the top of the README.md file in your project code. This link will serve as the entry point for us to review and feature your project. ### Example README Section: ```markdown <!-- other README.md content --> <!-- make sure to keep this link at the top to make it visible --> ## Deployed Link Visit my app: [https://my-social-media-app.example.com](https://my-social-media-app.example.com) <!-- other README.md content --> ``` ### Why to get featured? 1. **Contribution in Open Source:** - Gain recognition and celebrate contributions within the FreeAPI community. - Enhance your portfolio with real-world open source projects. 2. **Deployed Project:** - Gain hands-on experience with real-world deployment challenges. - Enhance your portfolio with a deployed and functional application. 3. **Showcasing Your Work:** - Increase visibility by being featured on the official FreeAPI website. - Receive recognition from the FreeAPI community. 4. **Networking Opportunities:** - Connect with like-minded individuals within the FreeAPI community. - Showcase your skills to potential collaborators, mentors, or employers. 5. **Beta Stage Advantage:** - Gain early recognition as an early adopter and contributor. - Influence the future development of the Featured Projects feature. 6. **Skill Showcase:** - Showcase proficiency in both backend consumption and frontend development. - Highlight your preferred frontend tech stack. ### What Happens Next: - **Beta Stage:** While this feature is in beta, our team will be actively monitoring deployed links mentioned in README.md files of frontend example projects. - **Official Rollout:** Once the Featured Projects page is officially launched on [FreeAPI](https://freeapi.app), we will review and select projects from the submitted links to showcase. So, you can start deploying your apps. - **Notification:** If your project is selected, we will notify you and include your project in the Featured Projects section on [FreeAPI](https://freeapi.app). ### Important Note: - Projects with deployed links mentioned at the top of the README.md file have a higher chance of being featured. - This feature is in beta, and we appreciate your early contributions to help shape the future of FreeAPI. The more links we get the sooner this section will get up and running. Thank you for being part of the FreeAPI community and contributing to the project! We look forward to featuring your fantastic work. 🌟 Happy coding! 🚀 ================================================ FILE: Dockerfile ================================================ FROM node:20.13.1-alpine RUN mkdir -p /usr/src/freeapi && chown -R node:node /usr/src/freeapi WORKDIR /usr/src/freeapi # Copy package json and yarn lock only to optimise the image building COPY package.json yarn.lock ./ # copy prepare.js prior. It will be executed after package installation and before ROOT dir is cloned COPY prepare.js ./ USER node RUN yarn install --pure-lockfile COPY --chown=node:node . . EXPOSE 8080 CMD [ "npm", "start" ] ================================================ FILE: LICENSE.md ================================================ MIT License FreeAPI Copyright (c) 2023 Hitesh Choudhary Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # FreeAPI.app ## Problem We are trying to build a single source API hub that can be used to learn api handling in any programming language. Users can build their front end portfolio in web and mobile apps using this api hub. # What is FreeAPI.app The FreeAPI project is an innovative and community-driven initiative aimed at providing developers with free and accessible APIs for their projects. The project focuses on delivering a wide range of APIs that cater to various domains and functionalities, enabling developers to seamlessly integrate these APIs into their applications. Key highlights of the FreeAPI project include: 1. **Accessibility:** The FreeAPI project is committed to eliminating barriers by providing free access to its collection of APIs. Developers can leverage these APIs without any cost limitations, allowing them to experiment, learn, and build innovative applications. 2. **Diverse API Collection:** The project offers a diverse and comprehensive collection of APIs that span across different industries, domains, and functionalities. Whether you require social media integrations, payment gateways, machine learning algorithms, or IoT device connectivity, the FreeAPI project has you covered. 3. **Simplified Integration:** The FreeAPI project understands the challenges developers face when integrating APIs into their applications. To address this, the project provides clear documentation, code samples, and SDKs, simplifying the integration process and reducing development time and effort. 4. **Community-Driven Development:** The project fosters a vibrant and collaborative community of developers. Contributors are encouraged to share their knowledge, engage in discussions, and collaborate on API-related projects. This collective effort ensures the continuous improvement and reliability of the APIs offered by the FreeAPI project. 5. **Learning and Skill Development:** The FreeAPI project aims to empower developers by providing a platform for learning and skill development. Through access to various APIs and educational resources, developers can enhance their understanding of API integration, expand their knowledge, and showcase their expertise through building complete projects. Overall, the FreeAPI project is a valuable resource for developers seeking accessible and diverse APIs. By fostering a supportive community, the project empowers developers to learn, create, and innovate, ultimately contributing to the growth and advancement of the API integration landscape. ## Features: Introducing our groundbreaking open source API hub project, a dynamic platform designed to revolutionize the way developers interact with APIs. With an emphasis on openness, accessibility, and learning, our API hub empowers developers of all levels to explore, experiment, and grow their skills in API integration. Highlights: 1. **Open Source:** Our API hub is built on the principles of open source, ensuring transparency, collaboration, and community-driven development. This means that the source code is freely available, allowing developers to customize, extend, and contribute to the project. 2. **Free to Use:** We firmly believe in removing barriers to entry, which is why our API hub is completely free to use. Whether you're a seasoned developer or just starting your coding journey, you can leverage our platform without any cost limitations. 3. **Local or Deployment**: Flexibility is at the core of our API hub. You have the option to use it locally, running on your own machine, or deploy it to a server, making it accessible to others. This versatility ensures that you can adapt the platform to your specific development environment. 4. **Learning Resource**: Our API hub is designed as a comprehensive learning resource, offering a wealth of educational materials, tutorials, and documentation. Whether you're a beginner or seeking to expand your API knowledge, our platform provides the resources you need to learn and improve. 5. **Custom Endpoints for Beginners**: For developers at the beginner level, our API hub offers custom endpoints that provide a hands-on experience in handling API responses. These beginner-friendly APIs allow you to practice and familiarize yourself with the basics of working with APIs. 6. **Advanced APIs for Portfolio Building**: In addition to beginner-level endpoints, our API hub also provides advanced APIs to challenge and stretch your skills. These APIs enable you to tackle more complex integration scenarios, helping you build a robust portfolio of projects to showcase your expertise. By combining open source principles, accessibility, and a focus on learning, our API hub project paves the way for developers to thrive in the world of API integration. Join our vibrant community and embark on an exciting journey of discovery, growth, and innovation. # ⚠️ Important Note: Avoiding Data Loss and Self-Hosting # Background: Our open-source project is currently hosted on a remote server, where we are forced to reset the entire server, **including the file system and MongoDB database**, every **2 hours** to avoid incurring additional costs. This process results in the **deletion of all image/static files and a reset of the entire database on the server.** ## What does this mean for you? **Data Loss:** Any changes made during the 2-hour interval (on the remote server), including uploaded images and user data, will be lost and unrecoverable. **Service Interruption:** The server reset might disrupt your development and testing processes for a certain duration while the server is rebooting (for 1-2 minutes). ## Recommended Solutions: **Local API Usage:** For development and testing purposes, we strongly recommend using the API locally on your machine by **cloning the project**. This ensures that your work is not affected by the server resets and allows you to maintain a stable development environment on your local machine. **Self-Hosting on Railway _(recommended for personal projects)_:** To self-host the FreeAPI.app application, you can take advantage of a pre-built template that is readily available. [Click here for detailed docs](https://github.com/hiteshchoudhary/apihub/#-using-railway-one-click-deploy) # 🏁 Installation ### 📦 Using Docker (recommended) To run the FreeAPI project, follow these steps: 1. Install [Docker](https://www.docker.com/) on your machine. 2. Clone the project repository. 3. Navigate to the project directory. 4. Create `.env` file in the root folder and copy paste the content of `.env.sample`, and add necessary credentials. 5. Run the Docker Compose command: ```bash docker-compose up --build --attach backend # --build: Rebuild the image and run the containers # --attach: only show logs of Node app container and not mongodb ``` 6. Access the project APIs at the specified endpoints. ### 💻 Running locally To run the FreeAPI project locally, follow these steps: 1. Install [Yarn](https://yarnpkg.com/), [NodeJs](https://www.nodejs.org/), [MongoDB](https://www.mongodb.com) and [MongoDB Compass (optional)](https://www.mongodb.com/products/compass) on your machine. 2. Clone the project repository. 3. Navigate to the project directory. 4. Create `.env` file in the root folder and copy paste the content of `.env.sample`, and add necessary credentials. 5. Install the packages: ```bash yarn install ``` 6. Run the project: ```bash yarn start ``` 7. Access the project APIs at the specified endpoints. ### 🚄 Using Railway (One-click Deploy) To self-host the FreeAPI.app application, you can take advantage of a pre-built template that is readily available. [![Deploy FreeAPI.app](https://railway.app/button.svg)](https://railway.app/template/B2f7Hq) 1. Click the button above to visit railway.app. 2. Click on the **Deploy Now** button. 3. (Optional) Sign in with GitHub to deploy. 4. Fill in the Repository details: - Specify the repo name (e.g., freeapi-app). - Checkmark for Public/Private repository. 5. For Environment variables, we have provided some default values in the `ENV` to reduce the burden, but some parameters are mandatory: - `PORT`: Do not change the value, let it be set to 8080 to view the swagger docs after deployment. - `MONGODB_URI`: Provide the MongoDB Atlas database URL. An example is prefilled for you, edit/update it to continue. - `NODE_ENV`: Default set to 'development' to view the logs. You may choose to change it to 'production' (make sure to add exact same word) to hide them. - `EXPRESS_SESSION_SECRET`: It is advised to change the default value to your own secret value. - `ACCESS_TOKEN_SECRET`: It is advised to change the default value to your own secret value. - `ACCESS_TOKEN_EXPIRY`: Set to 1 day as default. - `REFRESH_TOKEN_SECRET`: It is advised to change the default value to your own secret value. - `REFRESH_TOKEN_EXPIRY`: Set to 10 days as default. - `FREEAPI_HOST_URL`: Set it as generated railway url. 6. Once you fill in the required environment parameters, if you choose to add others such as PayPal, Google, and Razorpay, please proceed to mention your credentials in the form. 7. Click on the **Deploy** button to trigger the first build. - Monitor the server logs; if you come across any deployment problems, feel free to raise an issue for our team to look into. Note: Once the application is deployed, please wait for 3-5 minutes for the swagger docs to be available. # 🧪 Testing To ensure reliability & stability for our end users, we utilize Playwright, a powerful testing framework to automate testing across all endpoints. ### 💻 Run the Test Server Make sure to add `MONGO_MEMORY_SERVER_PORT=10000` (mongodb port for e2e testing) in your `.env` file. ```bash yarn start:test-server ``` ### 🧪 Run Tests ```bash yarn test:playwright ``` This will generate a Playwright report. To view this report run the following command ```bash yarn playwright show-report ``` Make sure all the test cases are passed whenever you make any changes. # How to contribute - Guidelines ## ⚡️ Contribute in core FreeAPI codebase: We welcome your interest in contributing to our open source project! To contribute to FreeAPI, please follow these steps: 1. Fork the repository. 2. Create a new branch for your feature or bug fix: `git checkout -b feat/your-feature-name` or `git checkout -b fix/your-bug-fix-name` and make your changes. 3. Run all the tests 🧪 before committing the changes and make sure all tests are passed. 4. After all tests are passed, commit your changes with a descriptive messages: `git commit -am 'add your commit message'` 5. For more details on the commit format and other guidelines, please refer to the [Contributor Guidelines](./CONTRIBUTING.md). 6. Push your changes to your forked repository: `git push origin feat/your-feature-name`. 7. Submit a pull request to the main repository, explaining the changes you've made and providing any necessary details. Here's a guide on how you can effectively contribute to our API hub: 1. Pull Requests for Readme Updates: Please refrain from sending pull requests solely for updating the project's readme file. While we appreciate the importance of clear and concise documentation, we prefer to focus on substantial code contributions and feature enhancements. 2. Grammar Updates: Our team values effective communication, but we're not grammar sticklers. You don't need to send pull requests solely for grammar fixes or minor language improvements. Instead, concentrate on the core functionalities and features of the project. 3. Avoid Updating Existing Public APIs: To maintain stability and consistency, we discourage direct updates to existing public APIs within the API hub. These APIs have been thoroughly tested and approved. However, if you encounter any bugs or issues, we encourage you to open an issue on our project's issue tracker to notify us. 4. Build New Project APIs: We encourage you to explore your creativity and contribute by building complete project APIs. These APIs should provide comprehensive solutions that can assist developers in constructing complex projects to showcase their skills and abilities. Your contributions in this area will greatly benefit the community. 5. Draft a Proposal and Discuss on Discord: Before diving into your project, we recommend drafting a proposal. This can include a mind map or outline of the API you intend to build and its potential benefits. Join our Discord community, where you can share your proposal, discuss ideas, and gather feedback from fellow contributors. Engaging in these discussions will enhance your backend portfolio and help shape the future direction of the project. We appreciate your enthusiasm and look forward to your valuable contributions to our open source API hub project. Together, we can foster a collaborative environment and make a significant impact in the API integration landscape. Click [here](https://github.com/hiteshchoudhary/apihub/blob/main/CONTRIBUTING.md) for detailed contribution guide. ## 🚀 Contribute by creating frontend application: Thank you for your interest in contributing to the FreeAPI project by creating frontend applications consuming FreeAPIs! Your efforts play a crucial role in enhancing the user experience and expanding the reach of our APIs. Please follow this guide to ensure a smooth and collaborative contribution process. Click [here](https://github.com/hiteshchoudhary/apihub/blob/main/CONTRIBUTING_FRONTEND.md) for detailed contribution guide for Frontend Developers 🚀! ## 🧪 Contribute in testing suite Thank you for your interest in contributing to the FreeAPI project to increase code coverage of our API service that helps us to ship robust endpoints that are battlefield tested. Please follow this guide to ensure a smooth and collaborative contribution process. Click [here](https://github.com/hiteshchoudhary/apihub/blob/main/CONTRIBUTING_CODE_COVERAGE.md) for detailed contribution guide for increasing code coverage. # 📜 Swagger Docs [Swagger Docs](https://api.freeapi.app): https://api.freeapi.app NOTE: Swagger docs are auto generated from the `swagger.yaml` file. While running the project locally, make sure you change the url to `http://localhost:<port_from_.env>/api/v1` in the swagger docs `servers/url` field. ================================================ FILE: docker-compose.prod.yml ================================================ version: '3.8' services: backend: image: freeapi-server build: . ports: - 8080:8080 volumes: - ./:/usr/src/freeapi - /usr/src/freeapi/node_modules env_file: - ./.env depends_on: - mongodb networks: - freeapi-internal mongodb: image: mongo container_name: mongodb volumes: - data:/data/db networks: - freeapi-internal networks: freeapi-internal: volumes: data: ================================================ FILE: docker-compose.yml ================================================ version: '3.8' services: backend: image: freeapi-server build: . ports: - 8080:8080 volumes: - ./:/usr/src/freeapi - /usr/src/freeapi/node_modules env_file: - ./.env depends_on: - mongodb mongodb: image: mongo container_name: mongodb volumes: - data:/data/db ports: - 27017:27017 volumes: data: ================================================ FILE: e2e/common.js ================================================ import dotenv from "dotenv"; dotenv.config({ path: "../.env", }); export const URL = `http://localhost:${process.env.PORT || 8080}`; export const getApiContext = async (playwright) => playwright.request.newContext({ baseURL: URL, }); ================================================ FILE: e2e/db.js ================================================ import mongoose, { mongo } from "mongoose"; import { MongoMemoryServer } from "mongodb-memory-server"; const MONGO_MEMORY_SERVER_PORT = process.env.MONGO_MEMORY_SERVER_PORT || 10000; const MONGODB_URL = `mongodb://127.0.0.1:${MONGO_MEMORY_SERVER_PORT}/`; let mongoServer = null; let dbInstance = undefined; const connectDB = async () => { try { await mongoose.disconnect(); mongoServer = await MongoMemoryServer.create({ instance: { port: +MONGO_MEMORY_SERVER_PORT, }, }); dbInstance = await mongoose.connect(MONGODB_URL); } catch (error) { console.error("Mongo db connect error: ", error); process.exit(1); } }; export const clearDB = async (collectionName = null) => { if (!dbInstance) { dbInstance = await mongoose.connect(MONGODB_URL); } const connection = mongoose.connection; if (collectionName) { await connection.db.collection(collectionName).deleteMany({}); } else { const collections = await connection.db.listCollections().toArray(); const collectionNames = collections.map((col) => col.name); for (let name of collectionNames) { await connection.db.collection(name).deleteMany({}); } } }; export default connectDB; ================================================ FILE: e2e/routes/apps/todo.test.js ================================================ import { test, expect } from "@playwright/test"; import { getApiContext } from "../../common.js"; import { clearDB } from "../../db.js"; let apiContext; let todoId = null; test.describe("Todo App", () => { test.beforeAll(async ({ playwright }) => { apiContext = await getApiContext(playwright); await clearDB(); }); test.afterAll(async ({}) => { await apiContext.dispose(); }); test.describe("GET:/api/v1/todos - Get All Todos", () => { test("should return all todos", async () => { const res = await apiContext.get(`/api/v1/todos`); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.data.length).toEqual(0); }); }); test.describe("POST:/api/v1/todos - Create Todo", () => { test("should create todo with valid data", async () => { const todo = { title: "test-todo-title", description: "test-todo-description", }; const res = await apiContext.post(`/api/v1/todos`, { data: todo, }); const json = await res.json(); expect(res.status()).toEqual(201); expect(json.statusCode).toEqual(201); expect(json.data).toMatchObject(todo); todoId = json.data._id; }); test("should return a 422 with title error when `title` is not provided", async () => { const todo = {}; const res = await apiContext.post(`/api/v1/todos`, { data: todo, }); const json = await res.json(); expect(res.status()).toEqual(422); expect(json.statusCode).toEqual(422); expect(json.errors).toContainEqual( expect.objectContaining({ title: expect.anything() }) ); }); test("should return a 422 with title and description error when `title` and `description` is empty", async () => { const todo = { title: "", description: "" }; const res = await apiContext.post(`/api/v1/todos`, { data: todo, }); const json = await res.json(); expect(res.status()).toEqual(422); expect(json.statusCode).toEqual(422); expect(json.errors).toContainEqual( expect.objectContaining({ title: expect.anything() }) ); expect(json.errors).toContainEqual( expect.objectContaining({ description: expect.anything() }) ); }); }); test.describe("PATCH:/api/v1/todos/:id - Update Todo", () => { const todo = { title: "update-test-todo-title", description: "update-test-todo-description", }; test("should update todo with valid data", async () => { const res = await apiContext.patch(`/api/v1/todos/${todoId}`, { data: todo, }); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.statusCode).toEqual(200); expect(json.data).toMatchObject(todo); }); test("should return a 422 with title and description error when `title` and `description` is empty", async () => { const todo = { title: "", description: "" }; const res = await apiContext.patch(`/api/v1/todos/${todoId}`, { data: todo, }); const json = await res.json(); expect(res.status()).toEqual(422); expect(json.statusCode).toEqual(422); expect(json.errors).toContainEqual( expect.objectContaining({ title: expect.anything() }) ); expect(json.errors).toContainEqual( expect.objectContaining({ description: expect.anything() }) ); }); }); test.describe("PATCH:/api/v1/todos/toggle/status/:id - Toggle todo status", () => { test("should toggle todo status", async () => { const res = await apiContext.patch( `/api/v1/todos/toggle/status/${todoId}` ); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.statusCode).toEqual(res.status()); expect(json.data.isComplete).toBeTruthy(); }); }); test.describe("GET:/api/v1/todos - Get All Todos", () => { test("should return todo when valid id passed", async () => { const res = await apiContext.get(`/api/v1/todos/${todoId}`); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.statusCode).toEqual(res.status()); }); test("should return 422 with todoId error when invalid id passed", async () => { const res = await apiContext.get(`/api/v1/todos/__1`); const json = await res.json(); expect(res.status()).toEqual(422); expect(json.statusCode).toEqual(res.status()); expect(json.errors).toContainEqual( expect.objectContaining({ todoId: expect.anything() }) ); }); }); test.describe("DELETE:/api/v1/todos/:id - Delete Todo", () => { test("should delete todo", async () => { const res = await apiContext.delete(`/api/v1/todos/${todoId}`); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.statusCode).toEqual(res.status()); }); }); }); ================================================ FILE: e2e/routes/healthcheck.test.js ================================================ import { test, expect } from "@playwright/test"; import { getApiContext } from "../common.js"; let apiContext; test.describe("Healthcheck", () => { test.beforeAll(async ({ playwright }) => { apiContext = await getApiContext(playwright); }); test.afterAll(async ({}) => { await apiContext.dispose(); }); test("should return ok", async ({ page }) => { const res = await apiContext.get("/api/v1/healthcheck"); expect(res.ok()).toBeTruthy(); }); }); ================================================ FILE: e2e/routes/seeds/chat-app.test.js ================================================ import { test, expect } from "@playwright/test"; import { getApiContext } from "../../common.js"; import { clearDB } from "../../db.js"; let apiContext; test.describe("Seed Chat App", () => { test.beforeAll(async ({ playwright }) => { apiContext = await getApiContext(playwright); await clearDB(); }); test.afterAll(async ({}) => { await apiContext.dispose(); }); test.describe("POST:/api/v1/seed/chat-app - Seed Chat", async () => { test("should seed Chat App DB", async ({ page }) => { const res = await apiContext.post("/api/v1/seed/chat-app"); expect(res.status()).toEqual(201); }); }); }); ================================================ FILE: e2e/routes/seeds/ecommerce.test.js ================================================ import { test, expect } from "@playwright/test"; import { getApiContext } from "../../common.js"; import { clearDB } from "../../db.js"; import { CATEGORIES_COUNT, PRODUCTS_COUNT, } from "../../../src/seeds/_constants.js"; let apiContext; test.describe("Seed Ecommerce App", () => { test.beforeAll(async ({ playwright }) => { apiContext = await getApiContext(playwright); await clearDB(); }); test.afterAll(async ({}) => { await apiContext.dispose(); }); test.describe("POST:/api/v1/seed/ecommerce - Seed Ecommerce", async () => { test("should return 0 products before seed", async ({ page }) => { const res = await apiContext.get( "/api/v1/ecommerce/products?page=1&limit=1" ); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.data.totalProducts).toEqual(0); }); test("should return 0 categories before seed", async ({ page }) => { const res = await apiContext.get( "/api/v1/ecommerce/categories?page=1&limit=1" ); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.data.totalCategories).toEqual(0); }); test("should seed ecommerce DB", async ({ page }) => { const res = await apiContext.post("/api/v1/seed/ecommerce"); expect(res.status()).toEqual(201); }); test(`should return ${PRODUCTS_COUNT} products after seed`, async ({ page, }) => { const res = await apiContext.get( "/api/v1/ecommerce/products?page=1&limit=1" ); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.data.totalProducts).toEqual(PRODUCTS_COUNT); }); test(`should return ${CATEGORIES_COUNT} categories after seed`, async ({ page, }) => { const res = await apiContext.get( "/api/v1/ecommerce/categories?page=1&limit=1" ); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.data.totalCategories).toEqual(CATEGORIES_COUNT); }); }); }); ================================================ FILE: e2e/routes/seeds/generated-credentials.test.js ================================================ import fs from "fs"; import { test, expect } from "@playwright/test"; import { getApiContext } from "../../common.js"; let apiContext; test.describe("Get credentials", () => { test.beforeAll(async ({ playwright }) => { apiContext = await getApiContext(playwright); }); test.afterAll(async ({}) => { await apiContext.dispose(); }); test.describe("GET:/api/v1/seed/generated-credentials - Get credentials", async () => { test("should return public/temp/seed-credentials.json content", async ({ page, }) => { const seedCredentialsText = fs.readFileSync( "./public/temp/seed-credentials.json", "utf8" ); const seedCredentials = JSON.parse(seedCredentialsText); const res = await apiContext.get("/api/v1/seed/generated-credentials"); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.data).toMatchObject(seedCredentials); }); }); }); ================================================ FILE: e2e/routes/seeds/social-media.test.js ================================================ import { test, expect } from "@playwright/test"; import { getApiContext } from "../../common.js"; import { clearDB } from "../../db.js"; import { SOCIAL_POSTS_COUNT } from "../../../src/seeds/_constants.js"; let apiContext; test.describe("Seed social-media App", () => { test.beforeAll(async ({ playwright }) => { apiContext = await getApiContext(playwright); await clearDB(); }); test.afterAll(async ({}) => { await apiContext.dispose(); }); test.describe("POST:/api/v1/seed/social-media - Seed social-media", async () => { test("should return 0 posts before seed", async ({ page }) => { const res = await apiContext.get( "/api/v1/social-media/posts?page=1&limit=1" ); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.data.totalPosts).toEqual(0); }); test("should seed social-media DB", async ({ page }) => { const res = await apiContext.post("/api/v1/seed/social-media"); expect(res.status()).toEqual(201); }); test(`should return ${SOCIAL_POSTS_COUNT} post after seed`, async ({ page, }) => { const res = await apiContext.get( "/api/v1/social-media/posts?page=1&limit=1" ); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.data.totalPosts).toEqual(SOCIAL_POSTS_COUNT); }); }); }); ================================================ FILE: e2e/routes/seeds/todo.test.js ================================================ import { test, expect } from "@playwright/test"; import { getApiContext } from "../../common.js"; import { clearDB } from "../../db.js"; import { TODOS_COUNT } from "../../../src/seeds/_constants.js"; let apiContext; test.describe("Seed Todo App", () => { test.beforeAll(async ({ playwright }) => { apiContext = await getApiContext(playwright); await clearDB(); }); test.afterAll(async ({}) => { await apiContext.dispose(); }); test.describe("POST:/api/v1/seed/todos - Seed Todos", async () => { test("should return 0 todos before seed", async ({ page }) => { const res = await apiContext.get("/api/v1/todos"); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.data.length).toEqual(0); }); test("should seed todo DB", async ({ page }) => { const res = await apiContext.post("/api/v1/seed/todos"); expect(res.status()).toEqual(201); }); test(`should return ${TODOS_COUNT} todos after seed`, async ({ page }) => { const res = await apiContext.get("/api/v1/todos"); const json = await res.json(); expect(res.status()).toEqual(200); expect(json.data.length).toEqual(TODOS_COUNT); }); }); }); ================================================ FILE: e2e/test-server.js ================================================ import dotenv from "dotenv"; dotenv.config({ path: "../.env", }); import { httpServer } from "../src/app.js"; import connectDB from "./db.js"; const PORT = process.env.PORT || 8080; /** * Starting from Node.js v14 top-level await is available and it is only available in ES modules. * This means you can not use it with common js modules or Node version < 14. */ const majorNodeVersion = +process.env.NODE_VERSION?.split(".")[0] || 0; const startServer = () => { httpServer.listen(PORT, () => { console.info(`📑 Visit the documentation at: http://localhost:${PORT}`); console.log("⚙️ Server is running on port: " + PORT); }); }; if (majorNodeVersion >= 14) { try { await connectDB(); startServer(); } catch (err) { console.log("Mongo db connect error: ", err); } } else { connectDB() .then(() => { startServer(); }) .catch((err) => { console.log("Mongo db connect error: ", err); }); } ================================================ FILE: examples/apps/auth/.gitkeep ================================================ ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/.eslintrc.cjs ================================================ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended", ], ignorePatterns: ["dist", ".eslintrc.cjs"], parser: "@typescript-eslint/parser", plugins: ["react-refresh"], rules: { "react-refresh/only-export-components": [ "warn", { allowConstantExport: true }, ], "@typescript-eslint/no-explicit-any": ["off"], }, }; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/README.md ================================================ # Installation and setup Create a `.env` file in the ROOT folder and copy past the content of the `.env.sample` file in it. Run the following commands to start the server ```bash npm install npm run dev # Make sure to keep the freeapi server running ``` ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/index.html ================================================ <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>FreeAPI | Chat app example
================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/package.json ================================================ { "name": "vite-chat-app", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { "@headlessui/react": "^1.7.16", "@heroicons/react": "^2.0.18", "axios": "^1.7.4", "moment": "^2.29.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.14.2", "socket.io-client": "^4.7.2" }, "devDependencies": { "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", "autoprefixer": "^10.4.14", "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "postcss": "^8.4.27", "tailwindcss": "^3.3.3", "typescript": "^5.0.2", "vite": "^4.5.4" }, "resolutions": { "cross-spawn": "^7.0.5", "nanoid": "^3.3.8" } } ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/App.tsx ================================================ // Importing required modules and components from the react-router-dom and other files. import { Routes, Route, Navigate } from "react-router-dom"; import Login from "./pages/login"; import Register from "./pages/register"; import ChatPage from "./pages/chat"; import { useAuth } from "./context/AuthContext"; import PrivateRoute from "./components/PrivateRoute"; import PublicRoute from "./components/PublicRoute"; // Main App component const App = () => { // Extracting 'token' and 'user' from the authentication context const { token, user } = useAuth(); return ( {/* Root route: Redirects to chat if the user is logged in, else to the login page */} ) : ( ) } > {/* Private chat route: Can only be accessed by authenticated users */} } /> {/* Public login route: Accessible by everyone */} } /> {/* Public register route: Accessible by everyone */} } /> {/* Wildcard route for undefined paths. Shows a 404 error */} 404 Not found

} />
); }; // Exporting the App component to be used in other parts of the application export default App; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/api/index.ts ================================================ // Import necessary modules and utilities import axios from "axios"; import { LocalStorage } from "../utils"; // Create an Axios instance for API requests const apiClient = axios.create({ baseURL: import.meta.env.VITE_SERVER_URI, withCredentials: true, timeout: 120000, }); // Add an interceptor to set authorization header with user token before requests apiClient.interceptors.request.use( function (config) { // Retrieve user token from local storage const token = LocalStorage.get("token"); // Set authorization header with bearer token config.headers.Authorization = `Bearer ${token}`; return config; }, function (error) { return Promise.reject(error); } ); // API functions for different actions const loginUser = (data: { username: string; password: string }) => { return apiClient.post("/users/login", data); }; const registerUser = (data: { email: string; password: string; username: string; }) => { return apiClient.post("/users/register", data); }; const logoutUser = () => { return apiClient.post("/users/logout"); }; const getAvailableUsers = () => { return apiClient.get("/chat-app/chats/users"); }; const getUserChats = () => { return apiClient.get(`/chat-app/chats`); }; const createUserChat = (receiverId: string) => { return apiClient.post(`/chat-app/chats/c/${receiverId}`); }; const createGroupChat = (data: { name: string; participants: string[] }) => { return apiClient.post(`/chat-app/chats/group`, data); }; const getGroupInfo = (chatId: string) => { return apiClient.get(`/chat-app/chats/group/${chatId}`); }; const updateGroupName = (chatId: string, name: string) => { return apiClient.patch(`/chat-app/chats/group/${chatId}`, { name }); }; const deleteGroup = (chatId: string) => { return apiClient.delete(`/chat-app/chats/group/${chatId}`); }; const deleteOneOnOneChat = (chatId: string) => { return apiClient.delete(`/chat-app/chats/remove/${chatId}`); }; const addParticipantToGroup = (chatId: string, participantId: string) => { return apiClient.post(`/chat-app/chats/group/${chatId}/${participantId}`); }; const removeParticipantFromGroup = (chatId: string, participantId: string) => { return apiClient.delete(`/chat-app/chats/group/${chatId}/${participantId}`); }; const getChatMessages = (chatId: string) => { return apiClient.get(`/chat-app/messages/${chatId}`); }; const sendMessage = (chatId: string, content: string, attachments: File[]) => { const formData = new FormData(); if (content) { formData.append("content", content); } attachments?.map((file) => { formData.append("attachments", file); }); return apiClient.post(`/chat-app/messages/${chatId}`, formData); }; const deleteMessage = (chatId: string, messageId: string) => { return apiClient.delete(`/chat-app/messages/${chatId}/${messageId}`); }; // Export all the API functions export { addParticipantToGroup, createGroupChat, createUserChat, deleteGroup, deleteOneOnOneChat, getAvailableUsers, getChatMessages, getGroupInfo, getUserChats, loginUser, logoutUser, registerUser, removeParticipantFromGroup, sendMessage, updateGroupName, deleteMessage, }; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/components/Button.tsx ================================================ import React from "react"; import { classNames } from "../utils"; const Button: React.FC< React.ButtonHTMLAttributes & { fullWidth?: boolean; severity?: "primary" | "secondary" | "danger"; size?: "base" | "small"; } > = ({ fullWidth, severity = "primary", size = "base", ...props }) => { return ( <> ); }; export default Button; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/components/Input.tsx ================================================ import React from "react"; import { classNames } from "../utils"; const Input: React.FC> = ( props ) => { return ( ); }; export default Input; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/components/Loader.tsx ================================================ const Loader = () => { return (
); }; export default Loader; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/components/PrivateRoute.tsx ================================================ // Import required modules and types from React and react-router-dom libraries import React, { ReactNode } from "react"; import { Navigate } from "react-router-dom"; // Import authentication context for retrieving user and token information import { useAuth } from "../context/AuthContext"; // Define a PrivateRoute component that wraps child components to ensure user authentication const PrivateRoute: React.FC<{ children: ReactNode }> = ({ children }) => { // Destructure token and user details from the authentication context const { token, user } = useAuth(); // If there's no token or user ID, redirect to the login page if (!token || !user?._id) return ; // If authenticated, render the child components return children; }; // Export the PrivateRoute component for use in other parts of the application export default PrivateRoute; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/components/PublicRoute.tsx ================================================ // Import necessary libraries and types import React, { ReactNode } from "react"; import { Navigate } from "react-router-dom"; import { useAuth } from "../context/AuthContext"; // Define the PublicRoute component which takes in children as its prop const PublicRoute: React.FC<{ children: ReactNode }> = ({ children }) => { // Destructure token and user from the authentication context const { token, user } = useAuth(); // If there is a valid token and user ID, navigate the user to the chat page if (token && user?._id) return ; // If no token or user ID exists, render the child components as they are return children; }; // Export the PublicRoute component for use in other parts of the application export default PublicRoute; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/components/Select.tsx ================================================ import { Combobox } from "@headlessui/react"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid"; import React, { useEffect, useState } from "react"; import { classNames } from "../utils"; const Select: React.FC<{ options: { value: string; label: string; }[]; value: string; onChange: (value: { value: string; label: string }) => void; placeholder: string; }> = ({ options, value, placeholder, onChange }) => { const [localOptions, setLocalOptions] = useState([]); useEffect(() => { setLocalOptions(options); }, [options]); return ( o.value === value)} onChange={(val: any) => onChange(val)} >
{ setLocalOptions( options.filter((op) => op.label.includes(e.target.value)) ); }} displayValue={(option: (typeof options)[0]) => option?.label} /> {localOptions.length > 0 && ( {localOptions.map((option) => ( classNames( "cursor-pointer relative rounded-2xl select-none py-4 pl-3 pr-9", active ? "bg-dark text-white" : "text-white" ) } > {({ active, selected }) => ( <> {option.label} {selected && ( )} )} ))} )}
); }; export default Select; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/components/chat/AddChatModal.tsx ================================================ import { Dialog, Switch, Transition } from "@headlessui/react"; import { UserGroupIcon, XCircleIcon, XMarkIcon, } from "@heroicons/react/20/solid"; import { Fragment, useEffect, useState } from "react"; import { createGroupChat, createUserChat, getAvailableUsers } from "../../api"; import { ChatListItemInterface } from "../../interfaces/chat"; import { UserInterface } from "../../interfaces/user"; import { classNames, requestHandler } from "../../utils"; import Button from "../Button"; import Input from "../Input"; import Select from "../Select"; const AddChatModal: React.FC<{ open: boolean; onClose: () => void; onSuccess: (chat: ChatListItemInterface) => void; }> = ({ open, onClose, onSuccess }) => { // State to store the list of users, initialized as an empty array const [users, setUsers] = useState([]); // State to store the name of a group, initialized as an empty string const [groupName, setGroupName] = useState(""); // State to determine if the chat is a group chat, initialized as false const [isGroupChat, setIsGroupChat] = useState(false); // State to store the list of participants in a group chat, initialized as an empty array const [groupParticipants, setGroupParticipants] = useState([]); // State to store the ID of a selected user, initialized as null const [selectedUserId, setSelectedUserId] = useState(null); // State to determine if a chat is currently being created, initialized as false const [creatingChat, setCreatingChat] = useState(false); // Function to fetch users const getUsers = async () => { // Handle the request to get available users requestHandler( // Callback to fetch available users async () => await getAvailableUsers(), null, // No loading setter callback provided // Success callback (res) => { const { data } = res; // Extract data from response setUsers(data || []); // Set users data or an empty array if data is absent }, alert // Use the alert as the error handler ); }; // Function to create a new chat with a user const createNewChat = async () => { // If no user is selected, show an alert if (!selectedUserId) return alert("Please select a user"); // Handle the request to create a chat await requestHandler( // Callback to create a user chat async () => await createUserChat(selectedUserId), setCreatingChat, // Callback to handle loading state // Success callback (res) => { const { data } = res; // Extract data from response // If chat already exists with the selected user if (res.statusCode === 200) { alert("Chat with selected user already exists"); return; } onSuccess(data); // Execute the onSuccess function with received data handleClose(); // Close the modal or popup }, alert // Use the alert as the error handler ); }; // Function to create a new group chat const createNewGroupChat = async () => { // Check if a group name is provided if (!groupName) return alert("Group name is required"); // Ensure there are at least 2 group participants if (!groupParticipants.length || groupParticipants.length < 2) return alert("There must be at least 2 group participants"); // Handle the request to create a group chat await requestHandler( // Callback to create a group chat with name and participants async () => await createGroupChat({ name: groupName, participants: groupParticipants, }), setCreatingChat, // Callback to handle loading state // Success callback (res) => { const { data } = res; // Extract data from response onSuccess(data); // Execute the onSuccess function with received data handleClose(); // Close the modal or popup }, alert // Use the alert as the error handler ); }; // Function to reset local state values and close the modal/dialog const handleClose = () => { // Clear the list of users setUsers([]); // Reset the selected user ID setSelectedUserId(""); // Clear the group name setGroupName(""); // Clear the group participants list setGroupParticipants([]); // Set the chat type to not be a group chat setIsGroupChat(false); // Execute the onClose callback/function onClose(); }; // useEffect hook to perform side effects based on changes in the component lifecycle or state/props useEffect(() => { // Check if the modal/dialog is not open if (!open) return; // Fetch users if the modal/dialog is open getUsers(); // The effect depends on the 'open' value. Whenever 'open' changes, the effect will re-run. }, [open]); return (
Create chat
Is it a group chat? {" "} {isGroupChat ? (
{ setGroupName(e.target.value); }} />
) : null}
setNewGroupName(e.target.value) } />
) : (

{groupDetails?.name}

{groupDetails?.admin === user?._id ? ( ) : null}
)}

Group · {groupDetails?.participants.length}{" "} participants


{" "} {groupDetails?.participants.length} Participants

{groupDetails?.participants?.map((part) => { return (

{part.username}{" "} {part._id === groupDetails.admin ? ( admin ) : null}

{part.email}
{groupDetails.admin === user?._id ? (
) : null}

); })} {groupDetails?.admin === user?._id ? (
{!addingParticipant ? ( ) : (
setLocalSearchQuery(e.target.value.toLowerCase()) } />
{loadingChats ? (
) : ( // Iterating over the chats array [...chats] // Filtering chats based on a local search query .filter((chat) => // If there's a localSearchQuery, filter chats that contain the query in their metadata title localSearchQuery ? getChatObjectMetadata(chat, user!) .title?.toLocaleLowerCase() ?.includes(localSearchQuery) : // If there's no localSearchQuery, include all chats true ) .map((chat) => { return ( n.chat === chat._id).length } onClick={(chat) => { if ( currentChat.current?._id && currentChat.current?._id === chat._id ) return; LocalStorage.set("currentChat", chat); currentChat.current = chat; setMessage(""); getMessages(); }} key={chat._id} onChatDelete={(chatId) => { setChats((prev) => prev.filter((chat) => chat._id !== chatId) ); if (currentChat.current?._id === chatId) { currentChat.current = null; LocalStorage.remove("currentChat"); } }} /> ); }) )}
{currentChat.current && currentChat.current?._id ? ( <>
{currentChat.current.isGroupChat ? (
{currentChat.current.participants .slice(0, 3) .map((participant, i) => { return ( ); })}
) : ( )}

{getChatObjectMetadata(currentChat.current, user!).title}

{ getChatObjectMetadata(currentChat.current, user!) .description }
0 ? "h-[calc(100vh-336px)]" : "h-[calc(100vh-176px)]" )} id="message-window" > {loadingMessages ? (
) : ( <> {isTyping ? : null} {messages?.map((msg) => { return ( ); })} )}
{attachedFiles.length > 0 ? (
{attachedFiles.map((file, i) => { return (
attachment
); })}
) : null}
{ if (e.target.files) { setAttachedFiles([...e.target.files]); } }} /> { if (e.key === "Enter") { sendChatMessage(); } }} />
) : (
No chat selected
)}
); }; export default ChatPage; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/pages/login.tsx ================================================ // Importing necessary components and hooks import { LockClosedIcon } from "@heroicons/react/20/solid"; import { useState } from "react"; import Button from "../components/Button"; import Input from "../components/Input"; import { useAuth } from "../context/AuthContext"; // Component for the Login page const Login = () => { // State to manage input data (username and password) const [data, setData] = useState({ username: "", password: "", }); // Accessing the login function from the AuthContext const { login } = useAuth(); // Function to update state when input data changes const handleDataChange = (name: string) => (e: React.ChangeEvent) => { setData({ ...data, [name]: e.target.value, }); }; // Function to handle the login process const handleLogin = async () => await login(data); return (

FreeAPI Chat App

Login

{/* Input for entering the username */} {/* Input for entering the password */} {/* Button to initiate the login process */} {/* Link to the registration page */} Don't have an account?{" "} Register
); }; export default Login; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/pages/register.tsx ================================================ // Import necessary components and hooks import { LockClosedIcon } from "@heroicons/react/20/solid"; import { useState } from "react"; import Button from "../components/Button"; import Input from "../components/Input"; import { useAuth } from "../context/AuthContext"; // Component for user registration const Register = () => { // State to manage user registration data const [data, setData] = useState({ email: "", username: "", password: "", }); // Access the register function from the authentication context const { register } = useAuth(); // Handle data change for input fields const handleDataChange = (name: string) => (e: React.ChangeEvent) => { // Update the corresponding field in the data state setData({ ...data, [name]: e.target.value, }); }; // Handle user registration const handleRegister = async () => await register(data); return ( // Register form UI

FreeAPI Chat App

{/* Lock icon */} Register

{/* Input fields for username, password, and email */} {/* Register button */} {/* Login link */} Already have an account?{" "} Login
); }; export default Register; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/utils/index.ts ================================================ // Importing necessary modules and interfaces import { AxiosResponse } from "axios"; import { FreeAPISuccessResponseInterface } from "../interfaces/api"; import { ChatListItemInterface } from "../interfaces/chat"; import { UserInterface } from "../interfaces/user"; // A utility function for handling API requests with loading, success, and error handling export const requestHandler = async ( api: () => Promise>, setLoading: ((loading: boolean) => void) | null, onSuccess: (data: FreeAPISuccessResponseInterface) => void, onError: (error: string) => void ) => { // Show loading state if setLoading function is provided setLoading && setLoading(true); try { // Make the API request const response = await api(); const { data } = response; if (data?.success) { // Call the onSuccess callback with the response data onSuccess(data); } } catch (error: any) { // Handle error cases, including unauthorized and forbidden cases if ([401, 403].includes(error?.response.data?.statusCode)) { localStorage.clear(); // Clear local storage on authentication issues if (isBrowser) window.location.href = "/login"; // Redirect to login page } onError(error?.response?.data?.message || "Something went wrong"); } finally { // Hide loading state if setLoading function is provided setLoading && setLoading(false); } }; // A utility function to concatenate CSS class names with proper spacing export const classNames = (...className: string[]) => { // Filter out any empty class names and join them with a space return className.filter(Boolean).join(" "); }; // Check if the code is running in a browser environment export const isBrowser = typeof window !== "undefined"; // This utility function generates metadata for chat objects. // It takes into consideration both group chats and individual chats. export const getChatObjectMetadata = ( chat: ChatListItemInterface, // The chat item for which metadata is being generated. loggedInUser: UserInterface // The currently logged-in user details. ) => { // Determine the content of the last message, if any. // If the last message contains only attachments, indicate their count. const lastMessage = chat.lastMessage?.content ? chat.lastMessage?.content : chat.lastMessage ? `${chat.lastMessage?.attachments?.length} attachment${ chat.lastMessage.attachments.length > 1 ? "s" : "" }` : "No messages yet"; // Placeholder text if there are no messages. if (chat.isGroupChat) { // Case: Group chat // Return metadata specific to group chats. return { // Default avatar for group chats. avatar: "https://via.placeholder.com/100x100.png", title: chat.name, // Group name serves as the title. description: `${chat.participants.length} members in the chat`, // Description indicates the number of members. lastMessage: chat.lastMessage ? chat.lastMessage?.sender?.username + ": " + lastMessage : lastMessage, }; } else { // Case: Individual chat // Identify the participant other than the logged-in user. const participant = chat.participants.find( (p) => p._id !== loggedInUser?._id ); // Return metadata specific to individual chats. return { avatar: participant?.avatar.url, // Participant's avatar URL. title: participant?.username, // Participant's username serves as the title. description: participant?.email, // Email address of the participant. lastMessage, }; } }; // A class that provides utility functions for working with local storage export class LocalStorage { // Get a value from local storage by key static get(key: string) { if (!isBrowser) return; const value = localStorage.getItem(key); if (value) { try { return JSON.parse(value); } catch (err) { return null; } } return null; } // Set a value in local storage by key static set(key: string, value: any) { if (!isBrowser) return; localStorage.setItem(key, JSON.stringify(value)); } // Remove a value from local storage by key static remove(key: string) { if (!isBrowser) return; localStorage.removeItem(key); } // Clear all items from local storage static clear() { if (!isBrowser) return; localStorage.clear(); } } ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: { colors: { primary: "#6b8afd", secondary: "#2e333d", dark: "#212328", danger: "#eb3330", success: "#4aac68", }, }, }, plugins: [], }; ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/apps/chat-app/web/react-vite-tailwind/vite.config.ts ================================================ import { defineConfig } from "vite"; import dns from "dns"; import react from "@vitejs/plugin-react"; dns.setDefaultResultOrder("verbatim"); // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], server: { host: "localhost", port: 3000, }, }); ================================================ FILE: examples/apps/ecommerce/.gitkeep ================================================ ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/.eslintrc.cjs ================================================ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', ], ignorePatterns: ['dist', '.eslintrc.cjs'], parser: '@typescript-eslint/parser', plugins: ['react-refresh'], rules: { 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, } ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/.gitignore ================================================ .env server # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/README.md ================================================ ## ecommerce frontend application This is an ecommerce client app which is made by consuming freeapi: https://github.com/hiteshchoudhary/apihub. ### Steps to run the code locally * Download the freeapi project from: https://github.com/hiteshchoudhary/apihub, and set the project up. * Make sure to configure the following: * **CORS_ORIGIN as the exact frontend endpoint**. * MAILTRAP credentials for emails. (Used in forgot password feature) * Paypal Client ID and Client Secret. * Google SSO Credentials for logging in with google. * In .env, Set CLIENT_SSO_REDIRECT_URL to / for redirects after logging in from google. example http://localhost:3000/ * Update the following in the freeapi project: * Add an environment variable in .env file for forgot password redirection, and point it to the /forgot-password, as follows: ```FORGOT_PASSWORD_REDIRECT_URL=http://localhost:3000/forgot-password``` * In src/controllers/apps/auth/user.controller.js file, Update the forgot password controller to use the above environment variable when sending an email by using ```${process.env.FORGOT_PASSWORD_REDIRECT_URL}/${unHashedToken}``` when calling the sendEmail function inside the controller. * Create a .env file in the root directory of this project, and copy paste the contents of .env.sample into it. * In .env, Replace: * VITE_SERVER_URI: With the path where freeapi server is running example http://localhost:8080 by default * VITE_PAYPAL_CLIENT_ID: With your own paypal client id. * Run ```npm i ``` * Run ```npm run dev```, the development server will start on port 3000: Visit http://localhost:3000 to view the client app. ### Credits * FreeAPI Project: https://github.com/hiteshchoudhary/apihub * API for countries & states: https://countriesnow.space/ * Design Inspiration: https://www.figma.com/community/file/1219312065205187851 ### Dependencies * [React](https://github.com/facebook/react) : v18.2.0 * [Vite](https://vitejs.dev/) : v5.0.12 * [TailwindCSS](https://github.com/tailwindlabs/tailwindcss) : v3.3.6 * State Management * [React Redux](https://github.com/reduxjs/react-redux) : v9.0.4 * [Redux Toolkit](https://github.com/reduxjs/redux-toolkit) : v2.0.1 * API Calls * [Axios](https://github.com/axios/axios) : v1.6.2 * Payments * [React Paypal JS](https://github.com/paypal/react-paypal-js) : v8.1.3 * [Paypal JS](https://github.com/paypal/paypal-js) : v8.0.0 * Date Picker * [React Day Picker](https://github.com/gpbl/react-day-picker) : v8.10.1 * [Date fns](https://github.com/date-fns/date-fns) : v3.6.0 * Multilingual * [i18n](https://github.com/i18next/i18next) : v23.7.11 * [react-i18next](https://github.com/i18next/react-i18next) : v6.20.1 * Form Handling * [React Hook Form](https://github.com/react-hook-form/react-hook-form) : v7.49.2 ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/index.html ================================================ Ecommerce
================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/package.json ================================================ { "name": "client_app", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "devhost": "vite --host", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { "@paypal/react-paypal-js": "^8.1.3", "@reduxjs/toolkit": "^2.0.1", "axios": "^1.7.4", "date-fns": "^3.6.0", "i18next": "^23.7.11", "i18next-http-backend": "^2.4.2", "moment": "^2.29.4", "react": "^18.2.0", "react-day-picker": "^8.10.1", "react-dom": "^18.2.0", "react-hook-form": "^7.49.2", "react-i18next": "^13.5.0", "react-redux": "^9.0.4", "react-router-dom": "^6.20.1" }, "devDependencies": { "@paypal/paypal-js": "^8.0.0", "@types/node": "^20.10.4", "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.16", "eslint": "^8.55.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "postcss": "^8.4.32", "prettier": "3.2.5", "tailwindcss": "^3.3.6", "typescript": "^5.2.2", "vite": "^5.2.14" }, "resolutions": { "cross-spawn": "^7.0.5", "nanoid": "^3.3.8" } } ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/public/locales/ar/translation.json ================================================ { "en": "english", "ar": "عربي", "hn": "हिन्दी", "select": "يختار", "infoHeaderMessage": "", "companyName": "ecomm", "home": "بيت", "contact": "اتصال", "about": "عن", "login": "تسجيل الدخول", "searchProductsPlaceholder": "ما الذي تبحث عنه؟", "companyAddress": "عنوان طويل عشوائي يأتي هنا", "companyEmail": "email@email.com", "companyPhone": "+00 000 0000", "copyrightMessage": "حقوق الطبع والنشر 2024. جميع الحقوق محفوظة ©", "support": "يدعم", "days": "أيام", "hours": "ساعات", "minutes": "دقائق", "seconds": "دقائق", "bannerPromotion": "ارفع أسلوبك", "bannerPromotion2": "مع أحدث الوافدين لدينا", "categories": "فئات", "browseByCategory": "تصفح حسب الفئة", "pleaseTryAgainLater": "فشل التحميل، يرجى المحاولة مرة أخرى في وقت لاحق", "addToCart": "أضف إلى السلة", "ourProducts": "منتجاتنا", "exploreOurProducts": "استكشاف منتجاتنا", "viewAllProducts": "عرض جميع المنتجات", "thisMonth": "هذا الشهر", "bestSellingProducts": "أفضل المنتجات مبيعا", "viewAll": "عرض الكل", "freeAndFastDelivery": "تسليم مجاني وسريع", "freeDeliveryFor": "التوصيل مجاني لجميع الطلبات التي تزيد عن 200", "247customerService": "خدمة العملاء 24/7", "247customerServiceDescription": "دعم عملاء ودود على مدار 24 ساعة طوال أيام الأسبوع", "guranteeHeading": "ضمان استعادة الاموال", "guranteeDescription": "نقوم بإرجاع الأموال خلال 30 يومًا", "loadMore": "تحميل المزيد", "inStock": "في الأوراق المالية", "outOfStock": "إنتهى من المخزن", "removeFromCart": "إزالة من العربة", "relatedItems": "الأصناف المتعلقة", "enterYourDetailsBelow": "أدخل التفاصيل الخاصة بك أدناه", "email": "بريد إلكتروني", "password": "كلمة المرور", "forgotPassword": "نسيت كلمة المرور", "dontHaveAnAccountSignUp": "ليس لديك حساب؟ قم بالتسجيل", "emailIsRequired": "البريد الالكتروني مطلوب", "passwordIsRequired": "كلمة المرور مطلوبة", "invalidEmailAddress": "عنوان البريد الإلكتروني غير صالح", "myAccount": "حسابي", "manageAccount": "إدارة الحساب", "logout": "تسجيل خروج", "confirmation": "تأكيد", "yes": "نعم", "no": "لا", "areYouSureYouWantToLogout": "هل أنت متأكد أنك تريد تسجيل الخروج ؟", "ok": "نعم", "signup": "اشتراك", "username": "اسم المستخدم", "usernameIsRequired": "اسم المستخدم مطلوب", "confirmPassword": "تأكيد كلمة المرور", "passwordsDontMatch": "كلمات المرور غير متطابقة", "alreadyHaveAnAccountLogin": "لديك حساب الآن؟ تسجيل الدخول هنا", "signupSuccessful": "الاشتراك بنجاح", "proceedToLogin": "انتقل إلى تسجيل الدخول", "updateQuantity": "تحديث الكمية", "cartUpdatedSuccessfully": "تم تحديث سلة التسوق بنجاح", "maxQuantityReached": "تم الوصول إلى الحد الأقصى، لا يمكن إضافة المزيد من الوحدات", "subtotal": "المجموع الفرعي", "discount": "تخفيض", "total": "المجموع", "proceedToCheckout": "الشروع في الخروج", "summary": "ملخص", "selectCountry": "حدد الدولة", "selectState": "اختر ولايه", "selectCity": "اختر مدينة", "enterAddress1": "أدخل سطر العنوان 1", "enterAddress2": "أدخل سطر العنوان 2", "enterPincode": "أدخل الرمز السري", "addAddress": "اضف عنوان", "add": "يضيف", "cancel": "يلغي", "addressIsRequired": "العنوان مطلوب", "countryIsRequired": "الدولة مطلوبة", "stateIsRequired": "الدولة مطلوبة", "cityIsRequired": "المدينة مطلوبة", "pincodeIsRequired": "مطلوب الرمز السري", "invalidPincode": "الرمز السري غير صالح", "addressAddedSuccessfully": "تمت إضافة العنوان بنجاح", "addressUpdatedSuccessfully": "تم تحديث العنوان بنجاح", "selectAddress": "حدد العنوان", "use": "يستخدم", "code": "شفرة", "toGet": "تحصل", "off": "عن", "minimumPurchaseOf": "دقيقة شراء", "enterCouponCode": "أدخل رمز القسيمة", "applyCode": "تطبيق الكود", "removeCouponCode": "إزالة رمز القسيمة", "codeIsRequired": "الرمز مطلوب", "couponCodeAppliedSuccessfully": "تم تطبيق رمز القسيمة بنجاح", "couponCodeRemovedSuccessfully": "تمت إزالة رمز القسيمة بنجاح", "update": "تحديث", "updateAddress": "عنوان التحديث", "areYouSureYouWantToDeleteTheAddress": "هل أنت متأكد أنك تريد حذف العنوان", "deleteAddress": "حذف العنوان", "addressDeletedSuccessfully": "تم حذف العنوان بنجاح", "selectAddressToContinueToPayment": "حدد العنوان لمواصلة الدفع", "failedToProcessPaymentTryAgain": "فشلت في معالجة الدفع، يرجى المحاولة مرة أخرى في وقت لاحق", "paymentSuccessfullyCompleted": "اكتمل الدفع بنجاح", "goToHome": "اذهب إلى المنزل", "myOrders": "طلباتي", "editYourProfile": "عدل ملفك الشخصي", "firstName": "الاسم الأول", "lastName": "اسم العائلة", "countryCode": "الرقم الدولي", "phoneNumber": "رقم التليفون", "firstNameIsRequired": "الإسم الأول مطلوب", "lastNameIsRequired": "إسم العائلة مطلوب", "countryCodeIsRequired": "مطلوب رمز البلد", "phoneNumberIsRequired": "رقم الهاتف مطلوب", "invalidPhoneNumber": "رقم الهاتف غير صحيح", "invalidCountryCode": "رمز البلد غير صالح", "profileUpdatedSuccessfully": "تم تحديث الملف الشخصي بنجاح", "failedToFetchInformation": "فشل في جلب المعلومات، يرجى المحاولة مرة أخرى في وقت لاحق", "signInWithGoogle": "الدخول مع جوجل", "noOrdersFound": "لم يتم العثور على أية طلبات", "searchResults": "نتائج البحث", "noResultsFound": "لم يتم العثور على نتائج", "editProfile": "تعديل الملف الشخصي", "myAddresses": "عناويني", "accountSettings": "إعدادت الحساب", "changePassword": "تغيير كلمة المرور", "currentPassword": "كلمة السر الحالية", "newPassword": "كلمة المرور الجديدة", "confirmNewPassword": "تأكيد كلمة المرور الجديدة", "thisFieldIsRequired": "هذه الخانة مطلوبه", "passwordChangedSuccessfully": "تم تغيير الرقم السري بنجاح", "usernameMustBeThreeCharactersLong": "يجب أن يتكون اسم المستخدم من 3 أحرف على الأقل", "submit": "يُقدِّم", "passwordResetEmailSent": "تم إرسال بريد إعادة تعيين كلمة المرور على معرف البريد الخاص بك", "pageNotFound": "الصفحة غير موجودة", "resetPassword": "إعادة تعيين كلمة المرور", "reset": "إعادة ضبط", "tryAgainLater": "حاول مرة أخرى في وقت لاحق", "resetPasswordSuccessful": "تمت إعادة تعيين كلمة المرور بنجاح", "minPrice": "سعر دقيقة", "maxPrice": "الحد الأقصى للسعر", "to": "ل", "filter": "منقي", "invalidValue": "قيمة غير صالحة", "maxValueIsLessThanMinValue": "القيمة القصوى أقل من القيمة الدنيا", "addressDeleted": "تم حذف العنوان", "selectRange": "اختر نطاقا", "pending": "قيد الانتظار", "cancelled": "ألغيت", "delivered": "تم التوصيل", "pleaseSelectAOrderStatus": "يرجى تحديد حالة الطلب", "ourStory": "قصتنا" } ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/public/locales/en/translation.json ================================================ { "en": "english", "ar": "عربي", "hn": "हिन्दी", "select": "Select", "infoHeaderMessage": "", "companyName": "ecomm", "home": "home", "contact": "contact", "about": "about", "login": "login", "searchProductsPlaceholder": "What are you looking for?", "companyAddress": "random long address comes here", "companyEmail": "email@email.com", "companyPhone": "+00 000 0000", "copyrightMessage": "© copyright right 2024. all right reserved", "support": "support", "days": "days", "hours": "hours", "minutes": "minutes", "seconds": "seconds", "bannerPromotion": "Elevate Your Style", "bannerPromotion2": "With Our Latest Arrivals", "categories": "categories", "browseByCategory": "browse by category", "pleaseTryAgainLater": "failed to load, please try again later", "addToCart": "add to cart", "ourProducts": "our products", "exploreOurProducts": "explore our products", "viewAllProducts": "view all products", "thisMonth": "this month", "bestSellingProducts": "best selling products", "viewAll": "view all", "freeAndFastDelivery": "free and fast delivery", "freeDeliveryFor": "free delivery for all orders over 200", "247customerService": "24/7 customer service", "247customerServiceDescription": "friendly 24/7 customer support", "guranteeHeading": "money back gurantee", "guranteeDescription": "we return money within 30 days", "loadMore": "load more", "inStock": "in stock", "outOfStock": "out of stock", "removeFromCart": "remove from cart", "relatedItems": "related items", "enterYourDetailsBelow": "enter your details below", "email": "email", "password": "password", "forgotPassword": "forgot password", "dontHaveAnAccountSignUp": "don't have an account ? signup", "emailIsRequired": "email is required", "passwordIsRequired": "password is required", "invalidEmailAddress": "invalid email address", "myAccount": "my account", "manageAccount": "manage account", "logout": "Logout", "confirmation": "confirmation", "yes": "yes", "no": "no", "areYouSureYouWantToLogout": "are you sure you want to logout ?", "ok": "ok", "signup": "signup", "username": "username", "usernameIsRequired": "username is required", "confirmPassword": "confirm password", "passwordsDontMatch": "passwords dont match", "alreadyHaveAnAccountLogin": "already have an account ?, login here", "signupSuccessful": "sign up successful", "proceedToLogin": "proceed to login", "updateQuantity": "update quantity", "cartUpdatedSuccessfully": "cart updated successfully", "maxQuantityReached": "max limit reached, cannot add any more units", "subtotal": "subtotal", "discount": "discount", "total": "total", "proceedToCheckout": "proceed to checkout", "summary": "summary", "selectCountry": "select country", "selectState": "select state", "selectCity": "select city", "enterAddress1": "enter address line 1", "enterAddress2": "enter address line 2", "enterPincode": "enter pincode", "addAddress": "add address", "add": "add", "cancel": "cancel", "addressIsRequired": "address is required", "countryIsRequired": "country is required", "stateIsRequired": "state is required", "cityIsRequired": "city is required", "pincodeIsRequired": "pincode is required", "invalidPincode": "invalid pincode", "addressAddedSuccessfully": "address added successfully", "addressUpdatedSuccessfully": "address updated successfully", "selectAddress": "select address", "use": "use", "code": "code", "toGet": "to get", "off": "off", "minimumPurchaseOf": "min purchase of", "enterCouponCode": "enter coupon code", "applyCode": "apply code", "removeCouponCode": "remove coupon code", "codeIsRequired": "code is required", "couponCodeAppliedSuccessfully": "coupon code applied successfully", "couponCodeRemovedSuccessfully": "coupon code removed successfully", "update": "update", "updateAddress": "update address", "areYouSureYouWantToDeleteTheAddress": "are you sure you want to delete the address", "deleteAddress": "delete address", "addressDeletedSuccessfully": "address deleted successfully", "selectAddressToContinueToPayment": "select address to continue to payment", "failedToProcessPaymentTryAgain": "failed to process payment, please try again later", "paymentSuccessfullyCompleted": "payment successfully completed", "goToHome": "go to home", "myOrders": "my orders", "editYourProfile": "edit your profile", "firstName": "first name", "lastName": "last name", "countryCode": "country code", "phoneNumber": "phone number", "firstNameIsRequired": "first name is required", "lastNameIsRequired": "last name is required", "countryCodeIsRequired": "country code is required", "phoneNumberIsRequired": "phone number is required", "invalidPhoneNumber": "invalid phone number", "invalidCountryCode": "invalid country code", "profileUpdatedSuccessfully": "profile updated successfully", "failedToFetchInformation": "failed to fetch information, please try again later", "signInWithGoogle": "sign in with google", "noOrdersFound": "no orders found", "searchResults": "search results", "noResultsFound": "no results found", "editProfile": "edit profile", "myAddresses": "my addresses", "accountSettings": "account settings", "changePassword": "change password", "currentPassword": "current password", "newPassword": "new password", "confirmNewPassword": "confirm new password", "thisFieldIsRequired": "this field is required", "passwordChangedSuccessfully": "password changed successfully", "usernameMustBeThreeCharactersLong": "username must be at least 3 characters long", "submit": "submit", "passwordResetEmailSent": "password reset mail has been sent on your mail id", "pageNotFound": "404: page not found", "resetPassword": "reset password", "reset": "reset", "tryAgainLater": "try again later", "resetPasswordSuccessful": "reset password successful", "minPrice": "min price", "maxPrice": "max price", "to": "to", "filter": "filter", "invalidValue": "invalid value", "maxValueIsLessThanMinValue": "max value is less than min value", "addressDeleted": "address deleted", "selectRange": "select range", "pending": "pending", "cancelled": "cancelled", "delivered": "delivered", "pleaseSelectAOrderStatus": "please select a order status", "ourStory": "our story" } ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/public/locales/hn/translation.json ================================================ { "en": "english", "ar": "عربي", "hn": "हिन्दी", "select": "चुनना", "infoHeaderMessage": "", "companyName": "ecomm", "home": "घर", "contact": "संपर्क", "about": "बारे में", "login": "लॉग इन", "searchProductsPlaceholder": "तुम क्या ढूंढ रहे हो?", "companyAddress": "पता", "companyEmail": "email@email.com", "companyPhone": "+00 000 0000", "copyrightMessage": "© कॉपीराइट अधिकार 2024. सर्वाधिकार सुरक्षित", "support": "सहायता", "days": "दिन", "hours": "घंटे", "minutes": "मिनट", "seconds": "सेकंड", "bannerPromotion": "अपनी शैली को उन्नत करें", "bannerPromotion2": "हमारे नवीनतम आगमन के साथ", "categories": "श्रेणियाँ", "browseByCategory": "वर्गानुसार खोजें", "pleaseTryAgainLater": "लोड करने में विफल, कृपया बाद में पुनः प्रयास करें", "addToCart": "कार्ट में जोड़ें", "ourProducts": "हमारे उत्पाद", "exploreOurProducts": "हमारे उत्पादों का अन्वेषण करें", "viewAllProducts": "सभी उत्पाद देखें", "thisMonth": "इस महीने", "bestSellingProducts": "सबसे ज्यादा बिकने वाले उत्पाद", "viewAll": "सभी को देखें", "freeAndFastDelivery": "मुफ़्त और तेज़ डिलीवरी", "freeDeliveryFor": "200 से अधिक के सभी ऑर्डर के लिए निःशुल्क डिलीवरी", "247customerService": "24/7 ग्राहक सेवा", "247customerServiceDescription": "अनुकूल 24/7 ग्राहक सहायता", "guranteeHeading": "पैसे वापसी की गारंटी", "guranteeDescription": "हम 30 दिनों के भीतर पैसा लौटा देते हैं", "loadMore": "और लोड करें", "inStock": "स्टॉक में", "outOfStock": "स्टॉक ख़त्म", "removeFromCart": "कार्ट से हटाएँ", "relatedItems": "संबंधित वस्तुएं", "enterYourDetailsBelow": "नीचे अपना विवरण दर्ज करें", "email": "ईमेल", "password": "पासवर्ड", "forgotPassword": "पासवर्ड भूल गए", "dontHaveAnAccountSignUp": "कोई खाता नहीं है? साइन अप करें", "emailIsRequired": "ईमेल की जरूरत है", "passwordIsRequired": "पासवर्ड की आवश्यकता है", "invalidEmailAddress": "अमान्य ईमेल पता", "myAccount": "मेरा खाता", "manageAccount": "खाते का प्रबंधन करें", "logout": "लॉग आउट", "confirmation": "पुष्टि", "yes": "हाँ", "no": "नहीं", "areYouSureYouWantToLogout": "क्या आप लॉग आउट करना चाहते हैं ?", "ok": "ठीक", "signup": "साइन अप", "username": "उपयोगकर्ता नाम", "usernameIsRequired": "उपयोगकर्ता नाम आवश्यक है", "confirmPassword": "पासवर्ड की पुष्टि", "passwordsDontMatch": "पासवर्ड मेल नहीं खाते", "alreadyHaveAnAccountLogin": "पहले से खाता है? लॉग इन करे", "signupSuccessful": "सफल पंजीकरण", "proceedToLogin": "लॉगिन करने के लिए आगे बढ़ें", "updateQuantity": "अद्यतन मात्रा", "cartUpdatedSuccessfully": "कार्ट सफलतापूर्वक अपडेट किया गया", "maxQuantityReached": "अधिकतम सीमा पूरी हो गई, अब और इकाइयां नहीं जोड़ी जा सकतीं", "subtotal": "उप-जॉड़", "discount": "छूट", "total": "कुल", "proceedToCheckout": "चेक आउट", "summary": "सारांश", "selectCountry": "देश चुनें", "selectState": "राज्य चुनें", "selectCity": "शहर चुनें", "enterAddress1": "पता पंक्ति 1 दर्ज करें", "enterAddress2": "पता पंक्ति 2 दर्ज करें", "enterPincode": "पिनकोड दर्ज करें", "addAddress": "पता जोड़ें", "add": "जोड़ना", "cancel": "रद्द", "addressIsRequired": "पता आवश्यक है", "countryIsRequired": "देश की आवश्यकता है", "stateIsRequired": "राज्य की आवश्यकता है", "cityIsRequired": "शहर की आवश्यकता है", "pincodeIsRequired": "पिनकोड आवश्यक है", "invalidPincode": "अमान्य पिनकोड", "addressAddedSuccessfully": "पता सफलतापूर्वक जोड़ा गया", "addressUpdatedSuccessfully": "पता सफलतापूर्वक अपडेट किया गया", "selectAddress": "पता चुनें", "use": "उपयोग", "code": "कोड", "toGet": "पाने के", "off": "छूट", "minimumPurchaseOf": "न्यूनतम खरीद", "enterCouponCode": "परचा कूट दर्ज करें", "applyCode": "कोड लागू करें", "removeCouponCode": "कूपन कोड हटाएँ", "codeIsRequired": "कोड आवश्यक है", "couponCodeAppliedSuccessfully": "कूपन कोड सफलतापूर्वक लागू हो गया", "couponCodeRemovedSuccessfully": "कूपन कोड सफलतापूर्वक हटा दिया गया", "update": "अद्यतन", "updateAddress": "पता अद्यतन करें", "areYouSureYouWantToDeleteTheAddress": "क्या आप वाकई पता हटाना चाहते हैं?", "deleteAddress": "पता हटाएं", "addressDeletedSuccessfully": "पता सफलतापूर्वक हटा दिया गया", "selectAddressToContinueToPayment": "भुगतान जारी रखने के लिए पता चुनें", "failedToProcessPaymentTryAgain": "भुगतान संसाधित करने में विफल, कृपया बाद में पुनः प्रयास करें", "paymentSuccessfullyCompleted": "भुगतान सफलतापूर्वक पूरा हुआ", "goToHome": "घर", "myOrders": "मेरे आदेश", "editYourProfile": "अपनी प्रोफ़ाइल को संपादित करें", "firstName": "पहला नाम", "lastName": "उपनाम", "countryCode": "कंट्री कोड", "phoneNumber": "फ़ोन नंबर", "firstNameIsRequired": "पहला नाम आवश्यक है", "lastNameIsRequired": "अंतिम नाम आवश्यक है", "countryCodeIsRequired": "देश कोड आवश्यक है", "phoneNumberIsRequired": "फ़ोन नंबर आवश्यक है", "invalidPhoneNumber": "अमान्य फ़ोन नंबर", "invalidCountryCode": "अमान्य देश कोड", "profileUpdatedSuccessfully": "प्रोफ़ाइल सफलतापूर्वक अपडेट किया गया", "failedToFetchInformation": "जानकारी प्राप्त करने में विफल, कृपया बाद में पुनः प्रयास करें", "signInWithGoogle": "गूगल से साइन इन करें", "noOrdersFound": "कोई आदेश नहीं मिला", "searchResults": "खोज के परिणाम", "noResultsFound": "कोई परिणाम नहीं मिले", "editProfile": "प्रोफ़ाइल संपादित करें", "myAddresses": "मेरे पते", "accountSettings": "खाता सेटिंग्स", "changePassword": "पासवर्ड बदलें", "currentPassword": "वर्तमान पासवर्ड", "newPassword": "नया पासवर्ड", "confirmNewPassword": "नए पासवर्ड की पुष्टि करें", "thisFieldIsRequired": "यह फ़ील्ड आवश्यक है", "passwordChangedSuccessfully": "पासवर्ड सफलतापूर्वक बदल गया", "usernameMustBeThreeCharactersLong": "उपयोगकर्ता नाम कम से कम 3 अक्षरों का होना चाहिए", "submit": "सबमिट", "passwordResetEmailSent": "पासवर्ड रीसेट मेल आपके मेल आईडी पर भेजा गया है", "pageNotFound": "404: पृष्ठ नहीं मिला", "resetPassword": "पासवर्ड रीसेट", "reset": "रीसेट", "tryAgainLater": "बाद में पुनः प्रयास करें", "resetPasswordSuccessful": "पासवर्ड रीसेट सफल", "minPrice": "न्यूनतम मूल्य", "maxPrice": "अधिकतम मूल्य", "to": "से", "filter": "फ़िल्टर", "invalidValue": "अमान्य मान", "maxValueIsLessThanMinValue": "अधिकतम मान न्यूनतम मान से कम है", "addressDeleted": "पता हटा दिया गया है", "selectRange": "सीमा चुनें", "pending": "अपूर्ण", "cancelled": "रद्द", "delivered": "वितरित", "pleaseSelectAOrderStatus": "कृपया एक आदेश की स्थिति चुनें", "ourStory": "हमारी कहानी" } ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/App.tsx ================================================ import RoutePaths from "./RoutePaths"; function App() { return ; } export default App; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/RoutePaths.tsx ================================================ import { BrowserRouter, Route, Routes } from "react-router-dom"; import PageLayout from "./layouts/PageLayout"; import HomePageContainer from "./pages/home/container/HomePageContainer"; import ProductsPageContainer from "./pages/products/container/ProductsPageContainer"; import { ROUTE_PATHS } from "./constants"; import ProductDetailPageContainer from "./pages/productdetail/container/ProductDetailPageContainer"; import LoginPageContainer from "./pages/login/container/LoginPageContainer"; import SignupPageContainer from "./pages/signup/container/SignupPageContainer"; import CartPageContainer from "./pages/cart/container/CartPageContainer"; import ForLoggedInUsers from "./protectedroutes/ForLoggedInUsers"; import CheckoutPageContainer from "./pages/checkout/container/CheckoutPageContainer"; import PaymentFeedbackPageContainer from "./pages/paymentfeedback/container/PaymentFeedbackPageContainer"; import ManageAccountPageContainer from "./pages/manageaccount/container/ManageAccountPageContainer"; import OrdersPageContainer from "./pages/orders/container/OrdersPageContainer"; import ProductSearchPageContainer from "./pages/productsearch/container/ProductSearchPageContainer"; import ResetForgottenPasswordPageContainer from "./pages/resetforgottenpassword/container/ResetForgottenPasswordPageContainer"; import PageNotFoundPageContainer from "./pages/pagenotfound/container/PageNotFoundPageContainer"; import OrderDetailPageContainer from "./pages/orderdetail/container/OrderDetailPageContainer"; import AboutPageContainer from "./pages/about/container/AboutPageContainer"; /* All Routes */ const RoutePaths = () => { return ( }> } /> } /> } /> } /> } /> } /> } /> }> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); }; export default RoutePaths; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/ArrowButton.tsx ================================================ import { useMemo } from "react"; import { ARROW_BUTTONS } from "../../constants"; import UpArrow from "../icons/UpArrow"; import LeftArrow from "../icons/LeftArrow"; interface ArrowButtonProps { type: ARROW_BUTTONS; onClickHandler: () => void; isDisabled: boolean; className?: string; } const ArrowButton = (props: ArrowButtonProps) => { const { type, onClickHandler, isDisabled, className } = props; const ArrowIcon = useMemo(() => { switch (type) { case ARROW_BUTTONS.UP: return ; case ARROW_BUTTONS.DOWN: return ; case ARROW_BUTTONS.LEFT: return ; case ARROW_BUTTONS.RIGHT: return ; } }, [type]); return ( ); }; export default ArrowButton; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/Button.tsx ================================================ import { ReactElement, useMemo } from "react"; import { ButtonTypes } from "../../constants"; import LoadingSpinner from "../icons/LoadingSpinner"; interface ButtonProps { buttonType?: ButtonTypes; children: ReactElement; className?: string; onClickHandler(): void; isLoading?: boolean; type?: "button" | "submit" | "reset" | undefined; isDisabled?: boolean; } const Button = (props: ButtonProps) => { const { buttonType = ButtonTypes.noBackgroundAndBorder, children, className, onClickHandler, isLoading = false, type = "button", isDisabled = false, } = props; // Styles based on type const buttonStyles = useMemo(() => { switch (buttonType) { case ButtonTypes.primaryButton: return "bg-darkRed text-zinc-50 rounded"; case ButtonTypes.secondaryButton: return "bg-white text-black font-poppinsMedium rounded border border-grey"; default: return ""; } }, [buttonType]); return ( ); }; export default Button; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/CarouselButtons.tsx ================================================ import { RefObject, useCallback, useEffect, useState } from "react"; import { useAppSelector } from "../../store"; import ArrowButton from "./ArrowButton"; import { ARROW_BUTTONS } from "../../constants"; interface CarouselButtons { scrollableElementRef: RefObject; } const CarouselButtons = (props: CarouselButtons) => { const { scrollableElementRef } = props; const isRTL = useAppSelector((state) => state.language.isRTL); /* Next & Back buttons disabled state */ const [isNextButtonDisabled, setIsNextButtonDisabled] = useState(false); const [isBackButtonDisabled, setIsBackButtonDisabled] = useState(true); /* Scroll to the right */ const onNextClickHandler = () => { if (scrollableElementRef.current) { scrollableElementRef.current.scrollBy({ left: 500, behavior: "smooth" }); } }; /* Scroll to the left */ const onBackClickHandler = () => { if (scrollableElementRef.current) { scrollableElementRef.current.scrollBy({ left: -500, behavior: "smooth" }); } }; const onElementScroll = useCallback(() => { if (scrollableElementRef.current) { /* Checking if there is more scrollable width: Disabling/Enabling Button based on it */ const scrollWidth = scrollableElementRef.current.scrollWidth; const clientWidth = scrollableElementRef.current.clientWidth; const scrollLeft = Math.abs(scrollableElementRef.current.scrollLeft); if (scrollLeft === 0) { /* 0 Scrolled */ setIsBackButtonDisabled(true); setIsNextButtonDisabled(false); } else if (scrollLeft + clientWidth === scrollWidth) { /* Fully Scrolled */ setIsNextButtonDisabled(true); setIsBackButtonDisabled(false); } else { /* In between */ setIsBackButtonDisabled(false); setIsNextButtonDisabled(false); } } }, [scrollableElementRef]); /* Listening for scroll event on the referenced element passed */ useEffect(() => { const elementRef = scrollableElementRef.current; if (elementRef) { elementRef.addEventListener("scroll", onElementScroll); } return () => { elementRef?.removeEventListener("scroll", onElementScroll); }; }, [scrollableElementRef, onElementScroll]); return (
); }; export default CarouselButtons; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/Checkbox.tsx ================================================ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { CHECKBOX_TYPE } from "../../constants"; import ErrorMessage from "./ErrorMessage"; export interface CheckboxActionsRef { forceSetCheckedItems(checkedItems: CHECKBOX_TYPE[]): void; } interface CheckboxProps { items: Array>; errorMessage?: string; containerClassName?: string; labelClassName?: string; checkboxContainerClassName?: string; onChange(checkedItems: Array>): void; actionsRef?: CheckboxActionsRef; } const Checkbox = React.forwardRef( (props: CheckboxProps, ref: React.ForwardedRef) => { const { items, errorMessage = "", labelClassName = "", containerClassName = "", checkboxContainerClassName = "", onChange, actionsRef, } = props; const {t} = useTranslation(); const [itemsChecked, setItemsChecked] = useState<{ items: CHECKBOX_TYPE[]; isInitialized: boolean; }>({ items: [], isInitialized: false }); useEffect(() => { /* If default checked items are not initialized */ if (!itemsChecked.isInitialized) { /* Filter checked items from items prop */ const defaultCheckedItems = items.filter( (item) => item.isDefaultSelected ); /* Initialize the default values */ setItemsChecked({ items: defaultCheckedItems, isInitialized: true }); } }, [items, itemsChecked.isInitialized]); /* To allow parent to forcefully set checked items: For operations like reset */ useEffect(() => { if (actionsRef) { actionsRef.forceSetCheckedItems = ( updatedItemsState: CHECKBOX_TYPE[] ) => { setItemsChecked({ items: [...updatedItemsState], isInitialized: true, }); }; } }, [actionsRef]); /* When itemsChecked changes call parent function */ useEffect(() => { onChange(itemsChecked.items); }, [itemsChecked.items, onChange]); /* On check / uncheck */ const checkboxChangeHandler = ( isChecked: boolean, item: CHECKBOX_TYPE ) => { /* on checking an item, push it into the items checked list */ if (isChecked) { setItemsChecked((prev) => { prev.items.push(item); return { items: [...prev.items], isInitialized: prev.isInitialized }; }); } else { /* Removing the item which is unchcked */ setItemsChecked((prev) => { prev.items = prev.items.filter( (checkedItem) => checkedItem.id !== item.id ); return { items: [...prev.items], isInitialized: prev.isInitialized }; }); } }; return (
{items.map((item) => (
checkboxChangeHandler(e.target.checked, item)} ref={ref} className="" /> {item.label ? ( ) : item.customElement ? (
{React.cloneElement(item.customElement, { data: item.data })}
) : ( <> )}
))}
{errorMessage && ( )}
); } ); export default Checkbox; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/DateRangePicker.tsx ================================================ import { ar } from "date-fns/locale/ar"; import { enIN } from "date-fns/locale/en-IN"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { DateRange, DayPicker } from "react-day-picker"; import "react-day-picker/dist/style.css"; import { useTranslation } from "react-i18next"; import useOutsideClick from "../../hooks/useOutsideClick"; import { useAppSelector } from "../../store"; import "../../styles/DateRangePicker.css"; import ErrorMessage from "./ErrorMessage"; export interface DateRangePickerActionsRef { forceSetSelectedRange(range: DateRange): void; } interface DateRangePickerProps { onChange(range: DateRange): void; errorMessage?: string; actionsRef?: DateRangePickerActionsRef; inputClassName?: string; containerClassName?: string; } const DateRangePicker = React.forwardRef( (props: DateRangePickerProps, _: React.ForwardedRef) => { const { onChange, errorMessage = "", actionsRef, containerClassName = "", inputClassName = "" } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); /* Visibility of date range picker */ const [isPickerShown, setIsPickerShown] = useState(false); /* To store the selected range of dates */ const [selectedRange, setSelectedRange] = useState(); /* Reference to the input element and div element which wraps around date range picker */ const inputRef = useRef(null); const datePickerRef = useRef(null); /* To know whether a click has occurred outside the date range picker */ const [clickedOutsidePicker] = useOutsideClick(datePickerRef); /* Toggle visibility to date range picker calendar */ const togglePicker = () => { setIsPickerShown((prev) => !prev); }; /* As month index starts from 0, adding 1 */ const getMonthValue = (month?: number): string => { if (typeof month === "number" && !isNaN(month)) { return `${month + 1}`; } else { return ""; } }; /* Value shown on the input */ const inputValue = useMemo(() => { return `${selectedRange?.from?.getDate()}/${getMonthValue(selectedRange?.from?.getMonth())}/${selectedRange?.from?.getFullYear()} - ${selectedRange?.to?.getDate()}/${getMonthValue(selectedRange?.to?.getMonth())}/${selectedRange?.to?.getFullYear()}`; }, [selectedRange]); /* If clicked outside, toggle picker */ useEffect(() => { if (clickedOutsidePicker) { togglePicker(); } }, [clickedOutsidePicker]); useEffect(() => { /* On change of selectedRange call parent onChange function */ if (selectedRange && selectedRange?.from && selectedRange?.to) { onChange(selectedRange); } }, [selectedRange, onChange]); /* To allow parent to forcefully set selected range: For operations like reset */ useEffect(() => { if (actionsRef) { actionsRef.forceSetSelectedRange = (range) => { setSelectedRange(range); }; } }, [actionsRef]); return (
{errorMessage && ( )} {isPickerShown && (
)}
); } ); export default DateRangePicker; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/Drawer.tsx ================================================ import { useTranslation } from "react-i18next"; import { DrawerOption, NavigationOption } from "../../constants"; import { useAppSelector } from "../../store"; import CloseIcon from "../icons/CloseIcon"; import NavList from "./NavList"; interface DrawerProps { headingText: string; navList?: Array; optionsList?: Array; onDrawerCloseHandler(): void; onOptionClickHandler?(option: DrawerOption): void; show: boolean; } const Drawer = (props: DrawerProps) => { const { headingText, navList, optionsList, onDrawerCloseHandler, onOptionClickHandler, show = false, } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); /* On click of an DrawerOption, call the function passed as prop */ const onOptionSelected = (option: DrawerOption) => { if (typeof onOptionClickHandler === "function") { onOptionClickHandler(option); } }; return ( ); }; export default Drawer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/Dropdown.tsx ================================================ import { ForwardedRef, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { DropdownItem, DropdownTypes } from "../../constants"; import useOutsideClick from "../../hooks/useOutsideClick"; import { useAppSelector } from "../../store"; import DownArrow from "../icons/DownArrow"; import ErrorMessage from "./ErrorMessage"; /* To expose Dropdown Actions to parents */ export interface DropdownActions { forceSetSelectedItem(item: DropdownItem): void; } interface DropdownProps { type: DropdownTypes; onChange(selectedItem: DropdownItem): void; label?: string; itemsList: Array; defaultSelectedItem?: DropdownItem; mainButtonClassNames?: string; errorMessage?: string; actionsRef?: DropdownActions; } const Dropdown = forwardRef( (props: DropdownProps, ref: ForwardedRef) => { const { type = DropdownTypes.noBorderDarkBg, onChange, label = "", itemsList, defaultSelectedItem, mainButtonClassNames = "", errorMessage = "", actionsRef, } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); /* Reference to the top container of dropdown */ const dropdownRef = useRef(null); /* To check for clicks outside the dropdown container, used to hide the dropdown menu */ const [clickedOutside] = useOutsideClick(dropdownRef); /* Selected Item state */ const [selectedItem, setSelectedItem] = useState( defaultSelectedItem ? defaultSelectedItem : null ); /* Dropdown menu visibility state */ const [isDropdownMenuShown, setIsDropdownMenuShown] = useState(false); /* Toggle dropdown menu visibility */ const toggleDropdownMenu = () => { setIsDropdownMenuShown((prev) => !prev); }; /* On click of an dropdown item */ const itemChangeHandler = (item: DropdownItem): void => { setSelectedItem(item); onChange(item); /* Hide dropdown menu */ toggleDropdownMenu(); }; /** * Text displayed against each option * If text value is present use that, * else use the textKey value to get the value from locales folder. */ const getDisplayedButtonText = useCallback( (item: DropdownItem): string => { if (item.text) { return item.text; } else if (item.textKey) { return t(item.textKey); } else { return ""; } }, [t] ); /* Styles for different dropdown types */ const typeStyles: { mainButton: string; textColor: string; menuContainer: string; labelText: string; } = useMemo(() => { switch (type) { case DropdownTypes.noBorderDarkBg: { return { mainButton: "border-none outline-none", textColor: "text-zinc-50", menuContainer: "bg-black", labelText: "text-zinc-50 capitalize", }; } case DropdownTypes.borderedLightBg: { return { mainButton: "border border-grey rounded px-2 py-1", textColor: "text-black", menuContainer: "bg-neutral-100 w-full shadow", labelText: "text-black capitalize", }; } default: return { mainButton: "", textColor: "", menuContainer: "", labelText: "", }; } }, [type]); /* Hiding Dropdown if a click is made outside */ useEffect(() => { if (clickedOutside) { setIsDropdownMenuShown(false); } }, [clickedOutside]); /* Default selection */ useEffect(() => { setSelectedItem(defaultSelectedItem ? defaultSelectedItem : null); }, [itemsList, defaultSelectedItem]); /* Providing implemetation of actions to the parent */ useEffect(() => { if (actionsRef) { actionsRef.forceSetSelectedItem = (item: DropdownItem) => { setSelectedItem(item); }; } }, [actionsRef]); return (
{label && ( )} {errorMessage && ( )} {isDropdownMenuShown && (
{/* Selected Item Comes First */} {selectedItem && (
)} {itemsList.map( (item) => item.id !== selectedItem?.id && (
) )}
)}
); } ); export default Dropdown; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/ErrorMessage.tsx ================================================ import { useAppSelector } from "../../store"; import ErrorIcon from "../icons/ErrorIcon"; interface ErrorMessageProps { message: string; className?: string; errorIconClassName?: string; isErrorIconShown?: boolean } const ErrorMessage = (props: ErrorMessageProps) => { const {message, className, errorIconClassName = 'w-8 h-8', isErrorIconShown = true} = props; const isRTL = useAppSelector((state) => state.language.isRTL); return (
{ isErrorIconShown && } {message}
) } export default ErrorMessage; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/FullPageLoadingSpinner.tsx ================================================ import LoadingSpinner from "../icons/LoadingSpinner"; import { createPortal } from "react-dom"; const FullPageLoadingSpinner = () => { return ( <> {createPortal(
, document.getElementById('root') || document.body )} ); }; export default FullPageLoadingSpinner; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/Hamburger.tsx ================================================ import { useCallback, useEffect, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; import { DrawerOption, NavigationOption } from "../../constants"; import useOutsideClick from "../../hooks/useOutsideClick"; import HamburgerIcon from "../icons/HamburgerIcon"; import Drawer from "./Drawer"; interface HamburgerProps { headingText: string; navList?: Array; optionsList?: Array } const Hamburger = (props: HamburgerProps) => { const { headingText, navList, optionsList } = props; /* Drawer visibility state */ const [isDrawerShown, setIsDrawerShown] = useState(false); const location = useLocation(); /* Toggle Drawer - toggles the drawer visibility state */ const toggleDrawer = useCallback(() => { setIsDrawerShown((prev) => !prev); }, [setIsDrawerShown]); /* Hamburger components top container reference */ const hamburgerRef = useRef(null); /* To check for clicks outside the hamburgers top container */ const [clickedOutside] = useOutsideClick(hamburgerRef); /* If outside click has occurred: Hiding the drawer */ useEffect(() => { /* Initially clickedOutside is 0: This prevents closing the drawer on first render */ if (!clickedOutside) { return; } /* Hide the drawer */ setIsDrawerShown(false); }, [clickedOutside]); /* Hide drawer once route changes */ useEffect(() => { setIsDrawerShown(false); }, [location]) return (
); }; export default Hamburger; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/Image.tsx ================================================ import { SyntheticEvent } from "react"; interface ImageProps { src: string, alt: string, backupImageSrc: string, className?: string } const Image = (props: ImageProps) => { const {src, alt, backupImageSrc, className = ''} = props const imgLoadErrorHandler = (event: SyntheticEvent) => { const target = event.currentTarget; target.onerror = null; target.src = backupImageSrc; } return ( {alt} ) } export default Image; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/Input.tsx ================================================ import { ForwardedRef, forwardRef, useId, useState } from "react"; import { useAppSelector } from "../../store"; import HidePasswordIcon from "../icons/HidePasswordIcon"; import ShowPasswordIcon from "../icons/ShowPasswordIcon"; import ErrorMessage from "./ErrorMessage"; interface InputProps { placeholder: string; type: string; className?: string; autoComplete?: string; errorMessage?: string; containerClassName?: string; } const Input = forwardRef( (props: InputProps, ref: ForwardedRef) => { const { placeholder = "", type = "text", className = "", containerClassName = "", autoComplete = "", errorMessage = "", ...otherProps } = props; /* Unique id */ const id = useId(); const isRTL = useAppSelector((state) => state.language.isRTL); /* Password visibility state */ const [isPasswordVisisble, setIsPasswordVisible] = useState(false); /* Toggle password visibility */ const togglePassword = () => { setIsPasswordVisible((prev) => !prev); }; /* Password Input */ if (type === "password") { return (
{errorMessage && }
); } return (
{errorMessage && }
); } ); export default Input; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/Link.tsx ================================================ import { useMemo } from "react"; import { LinkTypes } from "../../constants"; import { useAppSelector } from "../../store"; interface LinkProps { onClick?(): void; text: string; className?: string; linkType?: LinkTypes; } const Link = (props: LinkProps) => { const { onClick, text, linkType = LinkTypes.default, className = "" } = props; const isRTL = useAppSelector((state) => state.language.isRTL); const linkTypeStyles = useMemo(() => { switch (linkType) { case LinkTypes.red: return "text-darkRed hover:underline"; default: return ""; } }, [linkType]); return ( {text} ); }; export default Link; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/Modal.tsx ================================================ import { useMemo } from "react"; import { ButtonTypes } from "../../constants"; import Button from "./Button"; interface ModalProps { children: React.ReactElement; className?: string; heading?: string; primaryButtonText?: string; primaryButtonClassname?: string; secondaryButtonText?: string; secondaryButtonClassname?: string; primaryButtonHandler?(): void; secondaryButtonHandler?(): void; isPrimaryButtonLoading?: boolean; } const Modal = (props: ModalProps) => { const { children, className = "", heading = "", primaryButtonText = "", secondaryButtonText = "", primaryButtonClassname = "", secondaryButtonClassname = "", primaryButtonHandler, secondaryButtonHandler, isPrimaryButtonLoading = false, } = props; const isModalFooterShown = useMemo(() => { if ( (primaryButtonText && primaryButtonHandler) || (secondaryButtonText && secondaryButtonHandler) ) { return true; } return false; }, [ primaryButtonText, primaryButtonHandler, secondaryButtonText, secondaryButtonHandler, ]); return (
{heading && (
{heading}
)} {children} {isModalFooterShown && (
{primaryButtonText && primaryButtonHandler && ( )} {secondaryButtonText && secondaryButtonHandler && ( )}
)}
); }; export default Modal; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/NavItem.tsx ================================================ import { NavLink, useLocation } from "react-router-dom"; import { NavigationOption } from "../../constants"; import { useTranslation } from "react-i18next"; import { useMemo } from "react"; interface NavItemProps { navItem: NavigationOption; className?: string; } const NavItem = (props: NavItemProps) => { const { navItem, className = "" } = props; const { t } = useTranslation(); const { pathname, search } = useLocation(); const currentPath = useMemo(() => { return `${pathname}${search}`; }, [pathname, search]); return ( <>
`w-full lg:w-auto flex text-zinc-50 lg:text-black lg:before:absolute lg:before:-bottom-px lg:before:w-0 lg:before:h-px lg:before:bg-zinc-400 lg:before:transition-all lg:before:hover:w-full ${ /* Underline styles */ isActive ? "lg:hover:before:w-0 font-semibold lg:font-normal lg:underline lg:underline-offset-[7.4px] lg:decoration-1" : "" } `} to={navItem?.navigateTo} state={{ previousRoute: currentPath }} > {navItem?.icon} {t(navItem.textKey)}
); }; export default NavItem; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/NavList.tsx ================================================ import { NavigationOption } from "../../constants"; import { useAppSelector } from "../../store"; import NavItem from "./NavItem"; interface NavListProps { navList: Array; className?: string; } const NavList = ({ navList, className = "" }: NavListProps) => { const isRTL = useAppSelector((state) => state.language.isRTL); return (
{navList.map((navItem) => navItem?.customComponent ? (
{navItem.customComponent}
) : ( ) )}
); }; export default NavList; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/RadioButtons.tsx ================================================ import React, { useId } from "react"; import { RADIO_BUTTON_TYPE } from "../../constants"; import { useAppSelector } from "../../store"; import ErrorMessage from "./ErrorMessage"; interface RadioButtonsProps { items: Array>; containerClassName?: string; radioButtonContainerClassName?: string; onChange(selectedItem: T): void; errorMessage?: string; } const RadioButtons = React.forwardRef( ( props: RadioButtonsProps, ref: React.ForwardedRef ) => { const { items, containerClassName = "", radioButtonContainerClassName = "", onChange, errorMessage = "", } = props; const radioSetName = useId(); const isRTL = useAppSelector((state) => state.language.isRTL); return (
{errorMessage && ( )} {items.map((item) => (
{ onChange(item.data); }} /> {item.customElement ? (
{React.cloneElement(item.customElement, { data: item.data, })}
) : item.label ? ( ) : ( <> )}
))}
); } ); export default RadioButtons; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/RoundedIcon.tsx ================================================ interface RoundedIconProps { icon: React.ReactElement; } const RoundedIcon = (props: RoundedIconProps) => { const { icon } = props; return (
{icon}
); }; export default RoundedIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/SearchInput.tsx ================================================ import { FormEvent, useState } from "react"; import SearchIcon from "../icons/SearchIcon"; import { useAppSelector } from "../../store"; interface SearchInputProps { placeholder?: string; onChangeHandler?(searchInput: string): void; onBlurHandler?(searchInput: string): void; submitHandler?(searchInput: string): void; className?: string } const SearchInput = (props: SearchInputProps) => { const { placeholder, onChangeHandler, onBlurHandler, submitHandler, className ='' } = props; const isRTL = useAppSelector((state) => state.language.isRTL); const [inputValue, setInputValue] = useState(""); const onChange = (event: { target: { value: string } }) => { setInputValue(event?.target?.value); if (typeof onChangeHandler === "function") { onChangeHandler(event?.target?.value); } }; const onBlur = (event: { target: { value: string } }) => { if (typeof onBlurHandler === "function") { onBlurHandler(event?.target?.value); } }; const submit = (event: FormEvent) => { event.preventDefault(); if (typeof submitHandler === "function") { submitHandler(inputValue); } }; return (
); }; export default SearchInput; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/SelectionMenu.tsx ================================================ import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { BREAKPOINTS, SelectionMenuItem } from "../../constants"; import useBreakpointCheck from "../../hooks/useBreakpointCheck"; import useOutsideClick from "../../hooks/useOutsideClick"; import { useAppSelector } from "../../store"; import UpArrow from "../icons/UpArrow"; interface SelectionMenuProps { items: SelectionMenuItem[]; onItemSelect(item: SelectionMenuItem): void; heading: string; headingClassName?: string; } const SelectionMenu = (props: SelectionMenuProps) => { const { items, heading, onItemSelect, headingClassName = "capitalize", } = props; const isLG = useBreakpointCheck(BREAKPOINTS.lg); const { t } = useTranslation(); const isRTL = useAppSelector(state => state.language.isRTL); /* Menu visibility state */ const [isMenuShown, setIsMenuShown] = useState(false); /* Reference for the menu's top container */ const selectionMenuRef = useRef(null); /* To check clicks outside the menu's top container */ const [clickedOutside] = useOutsideClick(selectionMenuRef); /* Toggle menu visibility */ const toggleMenu = () => { setIsMenuShown((prev) => !prev); }; /* On click of menu item */ const menuItemClickHandler = (item: SelectionMenuItem) => { /* Hide the menu, call the function passed */ setIsMenuShown(false); onItemSelect(item); }; useEffect(() => { /* If an outside click has happened, hide the menu */ if (clickedOutside) { setIsMenuShown(false); } }, [clickedOutside]); return (
{isMenuShown && (
{items.map((item, index) => (
))}
)}
); }; export default SelectionMenu; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/TabItem.tsx ================================================ import { useTranslation } from "react-i18next"; import { TabItemConfig } from "../../constants"; import Text from "./Text"; interface TabItemProps { tabItem: TabItemConfig; onTabSelected(tab: TabItemConfig): void; isTabSelected: boolean; tabItemClassName?: string } const TabItem = ({ tabItem, onTabSelected, isTabSelected = false, tabItemClassName = "" }: TabItemProps) => { const { t } = useTranslation(); return ( ); }; export default TabItem; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/Tabs.tsx ================================================ import { useState } from "react"; import { TabItemConfig } from "../../constants"; import TabItem from "./TabItem"; import { useAppSelector } from "../../store"; interface TabsProps { tabsConfig: TabItemConfig[]; defaultSelectedTab?: TabItemConfig; onTabSelectionChanged(tabItem: TabItemConfig): void; tabsContainerClassName?: string; tabItemClassName?: string; } const Tabs = (props: TabsProps) => { const { tabsConfig, defaultSelectedTab = { id: -1, tabHeadingKey: "" }, onTabSelectionChanged, tabsContainerClassName = "", tabItemClassName = "" } = props; const isRTL = useAppSelector((state) => state.language.isRTL); /* Selected Tab */ const [selectedTab, setSelectedTab] = useState(defaultSelectedTab); /* Tab Change */ const tabChangeHandler = (tab: TabItemConfig) => { /* If the tab clicked is not equal to the currently selected tab */ if (tab.id !== selectedTab.id) { onTabSelectionChanged(tab); setSelectedTab(tab); } }; return (
{tabsConfig.map((tab) => ( ))}
); }; export default Tabs; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/Text.tsx ================================================ import { useAppSelector } from "../../store"; interface TextProps { className?: string; children: string; } const Text = (props: TextProps) => { const { className, children } = props; const isRTL = useAppSelector((state) => state.language.isRTL); return ( {children} ); }; export default Text; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/basic/ToastMessage.tsx ================================================ import { useMemo } from "react"; import { TOAST_MESSAGE_TYPES } from "../../constants"; import { useAppSelector } from "../../store"; import ErrorIcon from "../icons/ErrorIcon"; import TickIcon from "../icons/TickIcon"; interface ToastMessageProps { className?: string; } const ToastMessage = ({ className }: ToastMessageProps) => { /* Toast message state from redux */ const message = useAppSelector((state) => state.toastMessage); const isRTL = useAppSelector((state) => state.language.isRTL); /* Whether success message is shown */ const isSuccessMessage = useMemo(() => { if (message.type === TOAST_MESSAGE_TYPES.success) { return true; } return false; }, [message]); return ( <> {message.type && (
{isSuccessMessage ? ( ) : ( )} {message.message}
)} ); }; export default ToastMessage; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/AddressCard.tsx ================================================ import { useState } from "react"; import { AddressClass } from "../../services/address/AddressTypes"; import Button from "../basic/Button"; import DeleteIcon from "../icons/DeleteIcon"; import EditIcon from "../icons/EditIcon"; import AddAddressModalContainer from "../modals/addaddressmodal/container/AddAddressModalContainer"; import DeleteAddressModalContainer from "../modals/deleteaddressmodal/container/DeleteAddressModalContainer"; import { useAppSelector } from "../../store"; import Text from "../basic/Text"; interface AddressCardProps { address: AddressClass; className?: string; onAddressUpdated?(): void; } const AddressCard = (props: AddressCardProps) => { const { address, className = "shadow", onAddressUpdated } = props; const isRTL = useAppSelector((state) => state.language.isRTL); const [isAddressUpdateModalShown, setIsAddressUpdateModalShown] = useState(false); const [isDeleteAddressModalShown, setIsDeleteAddressModalShown] = useState(false); return ( <> {isAddressUpdateModalShown && ( setIsAddressUpdateModalShown(false)} address={address} onAddressAddedOrUpdatedCallback={onAddressUpdated} /> )} {isDeleteAddressModalShown && ( setIsDeleteAddressModalShown(false)} onAddressDeletedCallback={onAddressUpdated} /> )}
{`${address.state}, ${address.city}`} {address.country}
{address.addressLine1}
); }; export default AddressCard; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/AddressCardList.tsx ================================================ import { AddressClass } from "../../services/address/AddressTypes"; import AddressCard from "./AddressCard"; interface AddressCardListProps { addressList: AddressClass[]; addressListContainerClassName?: string; addressCardItemClassName?: string; onAddressUpdated?(): void; } const AddressCardList = (props: AddressCardListProps) => { const { addressList, addressListContainerClassName = "", addressCardItemClassName, onAddressUpdated, } = props; return (
{addressList.map((address) => ( ))}
); }; export default AddressCardList; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/CardContainer.tsx ================================================ import { RefObject } from "react"; import { ButtonTypes, CARD_CONTAINER_OPTION } from "../../constants"; import RectangleIcon from "../icons/RectangleIcon"; import Button from "../basic/Button"; import CarouselButtons from "../basic/CarouselButtons"; import { useAppSelector } from "../../store"; interface CardContainerProps { heading: string; subHeading?: string; extraOption?: CARD_CONTAINER_OPTION; children: React.ReactElement; carouselScrollableElementRef?: RefObject, extraOptionButtonText?: string; extraOptionButtonClickHandler?(): void; isLoadingButton?: boolean } const CardContainer = (props: CardContainerProps) => { const { heading, subHeading, extraOption, children, carouselScrollableElementRef, extraOptionButtonText, extraOptionButtonClickHandler, isLoadingButton = false } = props; const isRTL = useAppSelector((state) => state.language.isRTL); return (
{heading}
{subHeading && ( {subHeading} )}
{extraOption === CARD_CONTAINER_OPTION.CAROUSEL && carouselScrollableElementRef && ( )} {extraOption === CARD_CONTAINER_OPTION.RIGHT_BUTTON && extraOptionButtonText && extraOptionButtonClickHandler && ( )}
{children} {extraOption === CARD_CONTAINER_OPTION.BOTTOM_BUTTON && extraOptionButtonText && extraOptionButtonClickHandler && (
)}
); }; export default CardContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/CartItem.tsx ================================================ import { useMemo } from "react"; import { PUBLIC_IMAGE_PATHS } from "../../constants"; import { CartItemClass } from "../../services/cart/CartTypes"; import Button from "../basic/Button"; import Image from "../basic/Image"; import QuantityCounter from "./QuantityCounter"; import { DEFAULT_CURRENCY } from "../../data/applicationData"; import DeleteIcon from "../icons/DeleteIcon"; import { Product } from "../../services/product/ProductTypes"; import { useAppSelector } from "../../store"; import { useTranslation } from "react-i18next"; import { formatAmount } from "../../utils/commonHelper"; import Text from "../basic/Text"; interface CartItemProps { cartItem: CartItemClass; onQuantityChanged(product: Product, quantity: number): void; removeFromCart(product: Product): void; } const CartItem = (props: CartItemProps) => { const isRTL = useAppSelector((state) => state.language.isRTL); const { t } = useTranslation(); const { cartItem, onQuantityChanged, removeFromCart } = props; const product = useMemo(() => { return cartItem.product; }, [cartItem]); const subTotal = useMemo(() => { return cartItem.product.price * cartItem.quantity; }, [cartItem]); const isInStock = useMemo(() => { if (cartItem.quantity >= cartItem.product.stock) { return false; } return true; }, [cartItem]); const quantityChangeHandler = (quantity: number) => { if (quantity !== cartItem.quantity) { onQuantityChanged(product, quantity); } }; return (
{product.name}
{product.name} {`x ${cartItem.quantity}`} {formatAmount(subTotal, product.currency || DEFAULT_CURRENCY)} {!isInStock && ( {t("outOfStock")} )}
); }; export default CartItem; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/CartItemList.tsx ================================================ import { CartItemClass } from "../../services/cart/CartTypes"; import { Product } from "../../services/product/ProductTypes"; import CartItem from "./CartItem"; interface CartItemListProps { cartItems: CartItemClass[]; onQuantityChanged(product: Product, quantity: number): void; removeFromCart(product: Product): void; } const CartItemList = (props: CartItemListProps) => { const { cartItems, onQuantityChanged, removeFromCart } = props; return (
{cartItems.map((item) => (
))}
); }; export default CartItemList; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/CartSummary.tsx ================================================ import { useTranslation } from "react-i18next"; import { ButtonTypes } from "../../constants"; import { UserCart } from "../../services/cart/CartTypes"; import Button from "../basic/Button"; import InvoiceAmountSummary from "./InvoiceAmountSummary"; import Text from "../basic/Text"; import { DEFAULT_CURRENCY } from "../../data/applicationData"; interface CartSummaryProps { userCart: UserCart; checkoutClickHandler(): void; className?: string; } const CartSummary = (props: CartSummaryProps) => { const { userCart, checkoutClickHandler, className = "" } = props; const { t } = useTranslation(); return (
{t("summary")}
); }; export default CartSummary; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/CategoryCard.tsx ================================================ import { CATEGORY_ICONS } from "../../data/applicationData"; import { Category } from "../../services/category/CategoryTypes"; import GeneralCategoryIcon from "../icons/GeneralCategoryIcon"; import Button from "../basic/Button"; interface CategoryProps { category: Category; className?: string; categoryClickHandler?(category: Category): void } const CategoryCard = (props: CategoryProps) => { const { category, className = '', categoryClickHandler } = props; return ( ); }; export default CategoryCard; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/CompanyGurantee.tsx ================================================ import { useTranslation } from "react-i18next"; import { COMPANY_GURANTEE } from "../../constants"; import RoundedIcon from "../basic/RoundedIcon"; const CompanyGurantee = (props: COMPANY_GURANTEE) => { const {icon, headingKey, descriptionKey} = props const {t} = useTranslation(); return (
{t(headingKey)} {t(descriptionKey)}
) } export default CompanyGurantee; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/CouponCard.tsx ================================================ import { useTranslation } from "react-i18next"; import { CouponClass } from "../../services/coupon/CouponTypes"; interface CouponCardProps { coupon: CouponClass; className?: string; } const CouponCard = (props: CouponCardProps) => { const { coupon, className = '' } = props; const { t } = useTranslation(); return (
{`${t("use")} ${t("code")} ${coupon.couponCode}`} {`${t("toGet")} ${coupon.discountValue} ${t("off")}, ${t("minimumPurchaseOf")} ${coupon.minimumCartValue}`}
); }; export default CouponCard; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/CouponCardList.tsx ================================================ import { CouponClass } from "../../services/coupon/CouponTypes"; import { useAppSelector } from "../../store"; import CouponCard from "./CouponCard"; interface CouponCardListProps { coupons: Array; className?: string; childContainerClassName?: string; } const CouponCardList = (props: CouponCardListProps) => { const { coupons, className = "", childContainerClassName = "" } = props; const isRTL = useAppSelector(state => state.language.isRTL); return ( <> {coupons.length ? (
{coupons.map((coupon) => ( ))}
) : ( <> )} ); }; export default CouponCardList; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/FooterSection.tsx ================================================ import Text from "../basic/Text" interface FooterSectionProps { heading: string, children?: React.ReactElement | string, headingClassName?: string className?: string } const FooterSection = (props: FooterSectionProps) => { const {heading, children, headingClassName, className} = props return (
{heading}
{children}
) } export default FooterSection; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/InvoiceAmountSummary.tsx ================================================ import { useTranslation } from "react-i18next"; import { useAppSelector } from "../../store"; import { formatAmount } from "../../utils/commonHelper"; interface InvoiceAmountSummaryProps { total: number, discountedTotal: number; currency: string; className?: string } const InvoiceAmountSummary = (props: InvoiceAmountSummaryProps) => { const { total, discountedTotal, currency, className = '' } = props; const { t } = useTranslation(); const isRTL = useAppSelector(state => state.language.isRTL) return (
{t("subtotal")} {formatAmount(total, currency)}
{total !== discountedTotal && (
{t("discount")} {formatAmount(total - discountedTotal, currency)}
)}
{t("total")} {formatAmount(discountedTotal, currency)}
); }; export default InvoiceAmountSummary; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/OrderCard.tsx ================================================ import { useMemo } from "react"; import { OrderClass } from "../../services/order/OrderTypes"; import { convertUTCToLocalTime, formatDateTime } from "../../utils/dateTimeHelper"; import { DATE_TIME_FORMATS } from "../../constants"; import { capitalizeSentence, formatAmount } from "../../utils/commonHelper"; import { DEFAULT_CURRENCY } from "../../data/applicationData"; import { useAppSelector } from "../../store"; import { useTranslation } from "react-i18next"; import Button from "../basic/Button"; interface OrderCardProps { order: OrderClass; className?: string; orderClickHandler(order: OrderClass): void; } const OrderCard = (props: OrderCardProps) => { const { order, className = "", orderClickHandler } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); const displayedOrderDate = useMemo(() => { const orderDateInLocalTime = convertUTCToLocalTime(order.createdAt, DATE_TIME_FORMATS.standardDateWithTime); return formatDateTime( orderDateInLocalTime, DATE_TIME_FORMATS.standardDateWithTime, DATE_TIME_FORMATS.displayedDateWithTime ); }, [order]); const displayedOrderAddress = useMemo(() => { /* No address means address has been deleted by the user. */ if (!order?.address) { return capitalizeSentence(t("addressDeleted")); } return `${order?.address?.addressLine1 || ""} ${order?.address?.addressLine2 || ""}, ${order?.address?.state || ""}, ${order?.address?.country || ""}`; }, [order, t]); return ( ); }; export default OrderCard; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/OrderItem.tsx ================================================ import { useMemo } from "react"; import { PUBLIC_IMAGE_PATHS } from "../../constants"; import { OrderItemClass } from "../../services/order/OrderTypes"; import { formatAmount } from "../../utils/commonHelper"; import Image from "../basic/Image"; import Text from "../basic/Text"; import { DEFAULT_CURRENCY } from "../../data/applicationData"; import { useAppSelector } from "../../store"; interface OrderItemProps { orderItem: OrderItemClass; } const OrderItem = (props: OrderItemProps) => { const { orderItem } = props; const isRTL = useAppSelector((state) => state.language.isRTL); /* Subtotal: price * quantity */ const subTotal = useMemo(() => { return orderItem.product.price * orderItem.quantity; }, [orderItem]); return (
{orderItem.product.name}
{orderItem.product.name}
{`${formatAmount(orderItem.product.price, orderItem.product?.currency || DEFAULT_CURRENCY)}`} * {`${orderItem.quantity}`}
{formatAmount( subTotal, orderItem?.product?.currency || DEFAULT_CURRENCY )}
); }; export default OrderItem; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/OrderItemList.tsx ================================================ import { OrderItemClass } from "../../services/order/OrderTypes"; import OrderItem from "./OrderItem"; interface OrderItemListProps { itemList: Array; } const OrderItemList = (props: OrderItemListProps) => { const { itemList } = props; return (
{itemList.map((item) => (
))}
); }; export default OrderItemList; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/OrderListFilters.tsx ================================================ import { useTranslation } from "react-i18next"; import Button from "../basic/Button"; import DateRangePicker, { DateRangePickerActionsRef, } from "../basic/DateRangePicker"; import { ButtonTypes, OrderListFilterFields } from "../../constants"; import { useAppSelector } from "../../store"; import Checkbox, { CheckboxActionsRef } from "../basic/Checkbox"; import { ORDER_STATUS_FILTERS_CHECKBOX } from "../../data/applicationData"; import { Controller, useForm } from "react-hook-form"; import { useRef } from "react"; interface OrderListFiltersProps { orderFiltersSubmitHandler(fields: OrderListFilterFields): void; } const OrderListFilters = (props: OrderListFiltersProps) => { const { orderFiltersSubmitHandler } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); const { formState: { errors }, control, handleSubmit, setValue, } = useForm(); const checkboxActionsRef = useRef>({ forceSetCheckedItems(_) {}, }); const dateRangeActionsRef = useRef({ forceSetSelectedRange(_) {}, }); const resetHandler = () => { setValue("dateRange.from", undefined); setValue("dateRange.to", undefined); setValue("checkedStatus", ORDER_STATUS_FILTERS_CHECKBOX); dateRangeActionsRef.current.forceSetSelectedRange({ from: undefined, to: undefined, }); checkboxActionsRef.current.forceSetCheckedItems( ORDER_STATUS_FILTERS_CHECKBOX ); handleSubmit(orderFiltersSubmitHandler)(); }; return (
( )} /> { if (!value.length) { return t("pleaseSelectAOrderStatus"); } }, }} render={({ field }) => ( )} />
); }; export default OrderListFilters; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/OrderSummary.tsx ================================================ import { useTranslation } from "react-i18next"; import { OrderDetailClass } from "../../services/order/OrderTypes"; import Text from "../basic/Text"; import InvoiceAmountSummary from "./InvoiceAmountSummary"; import { DEFAULT_CURRENCY } from "../../data/applicationData"; interface OrderSummaryProps { orderDetail: OrderDetailClass; className?: string; } const OrderSummary = (props: OrderSummaryProps) => { const { orderDetail, className = "" } = props; const { t } = useTranslation(); return (
{t("summary")}
); }; export default OrderSummary; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/OrdersList.tsx ================================================ import { useTranslation } from "react-i18next"; import { OrderClass } from "../../services/order/OrderTypes"; import { useAppSelector } from "../../store"; import ErrorMessage from "../basic/ErrorMessage"; import OrderCard from "./OrderCard"; interface OrdersListProps { ordersList: Array; orderClickHandler(order: OrderClass): void; } const OrdersList = (props: OrdersListProps) => { const { ordersList, orderClickHandler } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); return ( <> {!ordersList?.length ? ( ) : (
{ordersList?.map((order) => (
))}
)} ); }; export default OrdersList; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/Payment.tsx ================================================ import { PayPalButtons, PayPalScriptProvider } from "@paypal/react-paypal-js"; import { useEffect, useMemo, useRef } from "react"; import OrderService from "../../services/order/OrderService"; import ApiError from "../../services/ApiError"; import { OnApproveData } from "@paypal/paypal-js/types/components/buttons"; import useCustomNavigate from "../../hooks/useCustomNavigate"; import { ROUTE_PATHS } from "../../constants"; import { useAppDispatch } from "../../store"; import { getUserCartThunk } from "../../store/CartSlice"; interface PaymentProps { addressId: string; isDisabled?: boolean } const Payment = (props: PaymentProps) => { const { addressId, isDisabled = false } = props; const navigate = useCustomNavigate(); const dispatch = useAppDispatch(); const paypalClientId = useMemo(() => { return import.meta.env.VITE_PAYPAL_CLIENT_ID; }, []); const addressIdRef = useRef(""); useEffect(() => { addressIdRef.current = addressId }, [addressId]) const createOrder = async () => { return OrderService.generatePayPalOrder(addressIdRef.current) .then((response) => { if (!(response instanceof ApiError)) { return response.id; } else { /* Error Here */ throw response; } }) .catch((error) => { console.error("Payment, Create Order Api Error", error); return ""; }); }; const onApprove = async (data: OnApproveData) => { return OrderService.verifyPayment(data.orderID).then((response) => { if (!(response instanceof ApiError)) { /* Payment Successful */ dispatch(getUserCartThunk()); navigate(ROUTE_PATHS.paymentFeedback, true, {checkoutDetails: {isSuccess: true}}) } else { console.error("Payment, On Approve Order Failed", response); /* Failed */ navigate(ROUTE_PATHS.paymentFeedback, true, {checkoutDetails: {isSuccess: false}}) } }); }; return (
); }; export default Payment; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/ProductCard.tsx ================================================ import { Product } from "../../services/product/ProductTypes"; import Image from "../basic/Image"; import { DEFAULT_CURRENCY } from "../../data/applicationData"; import { createSearchParams } from "react-router-dom"; import { PUBLIC_IMAGE_PATHS, QUERY_PARAMS, ROUTE_PATHS } from "../../constants"; import useCustomNavigate from "../../hooks/useCustomNavigate"; import { formatAmount } from "../../utils/commonHelper"; import { useAppSelector } from "../../store"; interface ProductCardProps { product: Product; className?: string; imageContainerClassName?: string; } const ProductCard = (props: ProductCardProps) => { const { product, className, imageContainerClassName } = props; const navigate = useCustomNavigate(); const isRTL = useAppSelector((state) => state.language.isRTL); /* navigate to /product?productId=&categoryId= */ const productClickHandler = () => { navigate({ pathname: ROUTE_PATHS.product, search: createSearchParams({ [QUERY_PARAMS.productId]: product._id, [QUERY_PARAMS.categoryId] : product.category }) .toString() }); }; return (
<>
{product.name}
{product.name}
{formatAmount(product.price, product.currency || DEFAULT_CURRENCY)} {product.previousPrice && ( {formatAmount(product.previousPrice, product.currency || DEFAULT_CURRENCY)} )}
); }; export default ProductCard; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/ProductFilters.tsx ================================================ import { useTranslation } from "react-i18next"; import { ButtonTypes, ProductFilterFields } from "../../constants"; import Input from "../basic/Input"; import { useForm } from "react-hook-form"; import Text from "../basic/Text"; import Button from "../basic/Button"; import { useAppSelector } from "../../store"; interface ProductFiltersProps { onFiltersChanged(fields: ProductFilterFields): void; resetFilterHandler(): void; className?: string; isLoading?: boolean; } const ProductFilters = (props: ProductFiltersProps) => { const { onFiltersChanged, resetFilterHandler, className = "", isLoading = false, } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); const { register, formState: { errors }, resetField, watch, handleSubmit, } = useForm(); /* Resetting the input fields and then calling the parent function */ const resetFilters = () => { resetField("price.min"); resetField("price.max"); resetFilterHandler(); }; return (
{ if (isNaN(Number(value)) || Number(value) < 0) { return t("invalidValue"); } }, })} /> {t("to")} { if ( isNaN(Number(value)) || Number(value) < Number(watch("price.min")) ) { return t("maxValueIsLessThanMinValue"); } }, })} />
); }; export default ProductFilters; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/ProductImagesView.tsx ================================================ import { useEffect, useState } from "react"; import { ImageClass } from "../../services/product/ProductTypes"; import Image from "../basic/Image"; import { useAppSelector } from "../../store"; interface ProductImagesViewProps { mainImage: ImageClass; subImages: Array; productName: string; className?: string; } const ProductImagesView = (props: ProductImagesViewProps) => { const { mainImage, subImages, productName, className } = props; const isRTL = useAppSelector((state) => state.language.isRTL); /* Top Image is the largest image shown */ const [topImage, setTopImage] = useState(mainImage); /* Other images */ const [otherImages, setOtherImages] = useState(subImages); /* To Change the main top image shown */ const changeTopImage = (newTopImage: ImageClass): void => { /* Replace the image clicked (newTopImage) with the top image, and set the top image to the image clicked (newTopImage) */ setOtherImages((prev) => { const imageIndex = prev.findIndex( (image) => image.url === newTopImage.url ); if (imageIndex !== -1) { prev[imageIndex] = topImage; setTopImage(newTopImage); } return prev; }); }; useEffect(() => { setTopImage(mainImage); }, [mainImage]); useEffect(() => { setOtherImages(subImages); }, [subImages]); return (
{productName}
{otherImages.map((otherImage) => ( ))}
); }; export default ProductImagesView; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/ProductList.tsx ================================================ import { Product } from "../../services/product/ProductTypes"; import { useAppSelector } from "../../store"; import ProductCard from "./ProductCard"; interface ProductListProps { products: Product[]; className?: string } const ProductList = (props: ProductListProps) => { const { products, className } = props; const isRTL = useAppSelector(state => state.language.isRTL) return (
{products.map((product) => (
))}
); }; export default ProductList; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/QuantityCounter.tsx ================================================ import { useEffect, useMemo, useState } from "react"; import AddIcon from "../icons/AddIcon"; import SubtractIcon from "../icons/SubtractIcon"; import { useAppSelector } from "../../store"; interface QuantityCounterProps { defaultQuantity: number; onQuantityChanged(quantity: number): void; maxLimit?: number; className?: string; textClassName?: string; } const QuantityCounter = (props: QuantityCounterProps) => { const { defaultQuantity, onQuantityChanged, className = "", textClassName = "", maxLimit = Infinity, } = props; const isRTL = useAppSelector(state => state.language.isRTL); const [quantity, setQuantity] = useState(defaultQuantity); /* Text Styles */ const textStyles = useMemo(() => { if(textClassName){ return textClassName; } return 'text-lg font-poppinsMedium' }, [textClassName]) /* If default quantity prop changes, update the quantity state */ useEffect(() => { setQuantity(defaultQuantity); }, [defaultQuantity]); /* On click of add quantity */ const addQuantity = () => { setQuantity((prev) => { prev++; return prev; }); }; /* On click of subtract quantity */ const subtractQuantity = () => { setQuantity((prev) => { if (prev !== 1) { prev--; } return prev; }); }; /* On change of quantity, call the parent onQuanityChanged function */ useEffect(() => { onQuantityChanged(quantity); }, [quantity, onQuantityChanged]); return (
{quantity}
); }; export default QuantityCounter; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/business/Timer.tsx ================================================ import moment from "moment"; import { useCallback, useEffect, useState } from "react"; import { DATE_TIME_FORMATS, DURATION } from "../../constants"; import { convertMillisecondsToDaysHoursMinsSec } from "../../utils/dateTimeHelper"; import { useTranslation } from "react-i18next"; import { useAppSelector } from "../../store"; import { zeroFormattedNumber } from "../../utils/commonHelper"; interface TimerProps { startTime: string; endTime: string; className?: string; timerContainerClassName?: string; } const Timer = (props: TimerProps) => { const { startTime, endTime, className = "", timerContainerClassName = "", } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); const [duration, setDuration] = useState({ days: -1, hours: -1, minutes: -1, seconds: -1, }); const calculateDuration = useCallback(() => { /* Converting to moment objects */ const start = moment.utc(startTime, DATE_TIME_FORMATS.standardDateWithTime); const end = moment.utc(endTime, DATE_TIME_FORMATS.standardDateWithTime); /* end - start in milliseconds */ const diffInMillis = end.diff(start); /* Converting millisecinds to days, hours, minutes and seconds */ const diffInDuration: DURATION = convertMillisecondsToDaysHoursMinsSec(diffInMillis); setDuration(diffInDuration); }, [startTime, endTime]); useEffect(() => { calculateDuration(); }, [calculateDuration]); return (
{duration?.days >= 0 && (
{zeroFormattedNumber(duration.days)} {t("days")}
)} {duration?.hours >= 0 && (
{zeroFormattedNumber(duration.hours)} {t("hours")}
)} {duration?.minutes >= 0 && (
{zeroFormattedNumber(duration.minutes)} {t("minutes")}
)} {duration?.seconds >= 0 && (
{zeroFormattedNumber(duration.seconds)} {t("seconds")}
)}
); }; export default Timer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/AccountIcon.tsx ================================================ const AccountIcon = (props: { className: string }) => { const { className = "" } = props; return ( ); }; export default AccountIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/AddIcon.tsx ================================================ const AddIcon = (props: { className: string }) => { const { className } = props; return ( ); }; export default AddIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/CartIcon.tsx ================================================ import { useAppSelector } from "../../store"; const CartIcon = (props: { className: string; quantity?: number }) => { const { className = "", quantity} = props; const isRTL = useAppSelector((state) => state.language.isRTL); return (
{quantity ? (
{quantity}
) : <>}
); }; export default CartIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/CloseIcon.tsx ================================================ const CloseIcon = ({className}: { className: string }) => { return ( ); }; export default CloseIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/DeleteIcon.tsx ================================================ const DeleteIcon = (props: { className: string }) => { const { className } = props; return ( ); }; export default DeleteIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/DownArrow.tsx ================================================ const DownArrow = (props: { className?: string }) => { const { className } = props; return ( ); }; export default DownArrow; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/EditIcon.tsx ================================================ const EditIcon = (props: { className: string }) => { const { className = "" } = props; return ( ); }; export default EditIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/ErrorIcon.tsx ================================================ const ErrorIcon = (props: { className: string }) => { const { className } = props; return ( ); }; export default ErrorIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/GeneralCategoryIcon.tsx ================================================ const GeneralCategoryIcon = (props: { className: string }) => { const { className } = props; return ( ); }; export default GeneralCategoryIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/GoogleIcon.tsx ================================================ const GoogleIcon = (props: { className: string }) => { const { className = "" } = props; return ( ); }; export default GoogleIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/GuranteeIcon.tsx ================================================ const GuranteeIcon = (props: {className: string}) => { const {className} = props return ( ); }; export default GuranteeIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/HamburgerIcon.tsx ================================================ const HamburgerIcon = ({className}: { className: string }) => { return ( ); }; export default HamburgerIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/HeadphoneIcon.tsx ================================================ const HeadphoneIcon = (props: {className: string}) => { const {className} = props return ( ); }; export default HeadphoneIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/HidePasswordIcon.tsx ================================================ const HidePasswordIcon = (props: { className: string }) => { const { className } = props; return ( ); }; export default HidePasswordIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/LeftArrow.tsx ================================================ const LeftArrow = (props: { className: string }) => { const { className } = props; return ( ); }; export default LeftArrow; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/LoadingSpinner.tsx ================================================ const LoadingSpinner = ({className}: { className: string }) => { return ( ); }; export default LoadingSpinner; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/LogoutIcon.tsx ================================================ const LogoutIcon = (props: { className: string }) => { const { className = "" } = props; return ( ); }; export default LogoutIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/OrderIcon.tsx ================================================ const OrderIcon = (props: { className: string }) => { const { className = '' } = props; return ( ); }; export default OrderIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/RectangleIcon.tsx ================================================ const RectangleIcon = (props: {className: string, rectClassName: string}) => { const {className, rectClassName} =props return ( ); }; export default RectangleIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/SearchIcon.tsx ================================================ const SearchIcon = (props: { className: string }) => { const { className } = props; return ( ); }; export default SearchIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/ShowPasswordIcon.tsx ================================================ const ShowPasswordIcon = (props: { className: string }) => { const { className } = props; return ( ); }; export default ShowPasswordIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/SubtractIcon.tsx ================================================ const SubtractIcon = (props: { className: string }) => { const { className } = props; return ( ); }; export default SubtractIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/TickIcon.tsx ================================================ const TickIcon = (props: { className: string; circleProps?: { cx: number; cy: number; r: number; className: string }; }) => { const { className, circleProps = { className: "fill-zinc-50", cx: 25, cy: 25, r: 25 }, } = props; return ( ); }; export default TickIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/TruckIcon.tsx ================================================ const TruckIcon = (props: {className: string}) => { const {className} = props return ( ); }; export default TruckIcon; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/icons/UpArrow.tsx ================================================ const UpArrow = (props: {className: string}) => { const {className} = props return ( ); }; export default UpArrow; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/modals/addaddressmodal/container/AddAddressModalContainer.tsx ================================================ import { useCallback, useEffect, useReducer, useState } from "react"; import { COUNTRIES_DROPDOWN_LIST } from "../../../../data/applicationData"; import AddAddressModal from "../presentation/AddAddressModal"; import { ADDRESS_FORM_KEYS, AddressFormFields, DropdownItem, } from "../../../../constants"; import CountryApiService from "../../../../services/countryapi/CountryApiService"; import ApiError from "../../../../services/ApiError"; import AddressService from "../../../../services/address/AddressService"; import FeedbackModal from "../../feedbackmodal/presentation/FeedbackModal"; import { useTranslation } from "react-i18next"; import { AddressClass } from "../../../../services/address/AddressTypes"; interface DropdownListActions { type: | "FETCHING" | "UPDATE_CITIES" | "UPDATE_STATES" | "UPDATE_LOADING_STATUS" | "UPDATE_ERROR_STATUS" | "COUNTRY_SELECTED" | "STATE_SELECTED" | "CITY_SELECTED"; payload?: DropdownItem[] | string | DropdownItem; } interface DropdownListState { selectedCountry: DropdownItem | undefined; selectedState: DropdownItem | undefined; selectedCity: DropdownItem | undefined; countries: DropdownItem[]; cities: DropdownItem[]; states: DropdownItem[]; isLoading: boolean; isError: boolean; errorMessage: string; } interface AddAddressModalContainerProps { hideModal(): void; onAddressAddedOrUpdatedCallback?(): void; address?: AddressClass /* Address if it exists: In case of Update */; } function dropdownListsReducer( state: DropdownListState, action: DropdownListActions ) { switch (action.type) { case "FETCHING": { return { ...state, isLoading: true, isError: false, errorMessage: "", }; } case "COUNTRY_SELECTED": { if ( typeof action.payload !== "string" && !(action.payload instanceof Array) ) { return { ...state, selectedCountry: action.payload, }; } return state; } case "STATE_SELECTED": { if ( typeof action.payload !== "string" && !(action.payload instanceof Array) ) { return { ...state, selectedState: action.payload, }; } return state; } case "CITY_SELECTED": { if ( typeof action.payload !== "string" && !(action.payload instanceof Array) ) { return { ...state, selectedCity: action.payload, }; } return state; } case "UPDATE_STATES": { if (action.payload instanceof Array) { return { ...state, states: action.payload, cities: [], isLoading: false, isError: false, errorMessage: "", }; } return state; } case "UPDATE_CITIES": { if (action.payload instanceof Array) { return { ...state, cities: action.payload, isLoading: false, isError: false, errorMessage: "", }; } return state; } case "UPDATE_ERROR_STATUS": { if (typeof action.payload === "string") { return { ...state, isLoading: false, isError: true, errorMessage: action.payload, }; } return state; } default: return state; } } const AddAddressModalContainer = (props: AddAddressModalContainerProps) => { const { hideModal = () => {}, onAddressAddedOrUpdatedCallback, address, } = props; const { t } = useTranslation(); /* State to hold initially selected address: In case of update (Default Address) */ const [initiallySelectedAddress, setInitiallySelectedAddress] = useState< { selectedCountry: DropdownItem; selectedState: DropdownItem; selectedCity: DropdownItem; addressLine1: string; addressLine2: string; pincode: string; } | undefined >(); /* Loading state for Saving the address to DB */ const [updateInProgress, setUpdateInProgress] = useState(false); const [isSuccessModalShown, setIsSuccessModalShown] = useState(false); const [state, dispatch] = useReducer(dropdownListsReducer, { selectedCountry: undefined, selectedState: undefined, selectedCity: undefined, countries: COUNTRIES_DROPDOWN_LIST, cities: [], states: [], isLoading: false, isError: false, errorMessage: "", }); const fetchStatesOfCountry = async (countryName: string) => { const response = await CountryApiService.getStatesOfACountry(countryName); if (!(response instanceof ApiError)) { const statesList = response.states.map((state) => { return { id: state.state_code, text: state.name, }; }); return statesList; } else { // Error dispatch({ type: "UPDATE_ERROR_STATUS", payload: response.errorResponse?.message || response.errorMessage, }); } }; const fetchCitiesOfState = useCallback( async (country: string | undefined, stateName: string) => { if (country) { const response = await CountryApiService.getCitiesOfAState( country, stateName ); if (!(response instanceof ApiError)) { const citiesList = response.map((city, index) => { return { id: index, text: city, }; }); return citiesList; } else { // Error dispatch({ type: "UPDATE_ERROR_STATUS", payload: response.errorResponse?.message || response.errorMessage, }); } } }, [] ); const dropdownChangeHandlers = async ( key: ADDRESS_FORM_KEYS, value: DropdownItem | undefined ) => { switch (key) { case ADDRESS_FORM_KEYS.country: { // Fetch States if (value?.text) { dispatch({ type: "COUNTRY_SELECTED", payload: value }); dispatch({ type: "FETCHING" }); const statesList = await fetchStatesOfCountry(value?.text); statesList && dispatch({ type: "UPDATE_STATES", payload: statesList }); } return; } case ADDRESS_FORM_KEYS.state: { // Fetch States if (value?.text) { dispatch({ type: "STATE_SELECTED", payload: value }); dispatch({ type: "FETCHING" }); const citiesList = await fetchCitiesOfState( state.selectedCountry?.text, value?.text ); citiesList && dispatch({ type: "UPDATE_CITIES", payload: citiesList }); } return; } case ADDRESS_FORM_KEYS.city: { // Fetch States if (value?.text) { dispatch({ type: "CITY_SELECTED", payload: value }); } return; } } }; const formSubmitHandler = async (data: AddressFormFields) => { setUpdateInProgress(true); let response: boolean | ApiError; if (address) { response = await AddressService.updateAddress( address?._id, data.country.text || "", data.state.text || "", data.city.text || "", data.addressLine1, data.addressLine2, data.pincode ); } else { response = await AddressService.createAddress( data.country.text || "", data.state.text || "", data.city.text || "", data.addressLine1, data.addressLine2, data.pincode ); } setUpdateInProgress(false); if (!(response instanceof ApiError)) { if (onAddressAddedOrUpdatedCallback) { onAddressAddedOrUpdatedCallback(); } // Success setIsSuccessModalShown(true); } else { dispatch({ type: "UPDATE_ERROR_STATUS", payload: response.errorResponse?.message || response.errorMessage, }); } }; useEffect(() => { const initializeStates = async () => { if (address) { /* Find selected country in countries dropdown list */ const countryFound = COUNTRIES_DROPDOWN_LIST.find( (country) => country.text === address.country ); if (!countryFound || !countryFound.text) { return; } /* Set selected country */ dispatch({ type: "COUNTRY_SELECTED", payload: countryFound }); /* Fetch states of preselected country */ dispatch({ type: "FETCHING" }); const statesList = await fetchStatesOfCountry(countryFound.text); if (!statesList) { return; } /* Find selected state in states list */ const stateFound = statesList.find( (state) => state.text === address.state ); if (!stateFound) { return; } /* Set states list and selected state */ dispatch({ type: "STATE_SELECTED", payload: stateFound }); dispatch({ type: "UPDATE_STATES", payload: statesList }); /* Fetch Cities */ dispatch({ type: "FETCHING" }); const citiesList = await fetchCitiesOfState( countryFound.text, stateFound.text ); if (!citiesList) { return; } /* Find Selected City In Cities List */ const cityFound = citiesList.find((city) => city.text === address.city); if (!cityFound) { return; } /* Set cities list and selected city */ dispatch({ type: "CITY_SELECTED", payload: cityFound }); dispatch({ type: "UPDATE_CITIES", payload: citiesList }); /* Set Initially selected address */ setInitiallySelectedAddress({ selectedCountry: countryFound, selectedState: stateFound, selectedCity: cityFound, addressLine1: address.addressLine1, addressLine2: address.addressLine2, pincode: address.pincode, }); } }; initializeStates(); }, [address, fetchCitiesOfState]); return ( <> {isSuccessModalShown ? ( ) : ( )} ); }; export default AddAddressModalContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/modals/addaddressmodal/presentation/AddAddressModal.tsx ================================================ import { useTranslation } from "react-i18next"; import { ADDRESS_FORM_KEYS, AddressFormFields, ButtonTypes, DropdownItem, DropdownTypes, REGEX_PATTERNS, } from "../../../../constants"; import Dropdown, { DropdownActions } from "../../../basic/Dropdown"; import Modal from "../../../basic/Modal"; import { Controller, useForm } from "react-hook-form"; import { useEffect, useRef } from "react"; import Input from "../../../basic/Input"; import FullPageLoadingSpinner from "../../../basic/FullPageLoadingSpinner"; import Button from "../../../basic/Button"; import ErrorMessage from "../../../basic/ErrorMessage"; import { useAppSelector } from "../../../../store"; interface AddAddressModalProps { initiallySelectedAddress?: { selectedCountry: DropdownItem; selectedCity: DropdownItem; selectedState: DropdownItem; addressLine1: string; addressLine2: string; pincode: string }; countriesList: Array; statesList: Array; citiesList: Array; dropdownChangeHandlers: ( key: ADDRESS_FORM_KEYS, value: DropdownItem | undefined ) => void; formSubmitHandler(data: AddressFormFields): void; fetchingDropdownLists: boolean; isModalButtonLoading: boolean; hideModal(): void; apiError?: string; } const AddAddressModal = (props: AddAddressModalProps) => { const { initiallySelectedAddress, countriesList = [], statesList = [], citiesList = [], dropdownChangeHandlers, formSubmitHandler, fetchingDropdownLists = false, isModalButtonLoading = false, hideModal, apiError = "", } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); const { handleSubmit, control, register, watch, formState: { errors }, setValue, } = useForm(); /* To check for initial country, city and state initialization */ const isInitializing = useRef(false); /* Refs to get access to dropdown actions */ const countryActionRef = useRef({forceSetSelectedItem(_) {},}); const stateActionRef = useRef({forceSetSelectedItem(_) {},}); const cityActionRef = useRef({forceSetSelectedItem(_) {},}); useEffect(() => { const subscription = watch((value, { name }) => { /* Call dropdownChangeHandler if initializing is not in progress (To avoid multiple API calls) */ if ((name === "country" || name === "city" || name === "state") && !isInitializing.current) { dropdownChangeHandlers(ADDRESS_FORM_KEYS[name], value[name]); } }); return () => { subscription.unsubscribe(); }; }, [watch, dropdownChangeHandlers, isInitializing]); useEffect(() => { if (initiallySelectedAddress) { /* Initializing in progress */ isInitializing.current = true; /* Set Dropdown values */ setValue("country", initiallySelectedAddress.selectedCountry); setValue("state", initiallySelectedAddress.selectedState); setValue("city", initiallySelectedAddress.selectedCity); setValue("addressLine1", initiallySelectedAddress.addressLine1); setValue("addressLine2", initiallySelectedAddress.addressLine2); setValue("pincode", initiallySelectedAddress.pincode); /* Set selected items on dropdown component */ countryActionRef.current?.forceSetSelectedItem(initiallySelectedAddress.selectedCountry); stateActionRef.current?.forceSetSelectedItem(initiallySelectedAddress.selectedState); cityActionRef.current.forceSetSelectedItem(initiallySelectedAddress.selectedCity); /* Initializing complete */ isInitializing.current = false; } }, [initiallySelectedAddress, setValue]); return ( <> {fetchingDropdownLists && } <> {apiError && ( )}
( )} /> ( )} /> ( )} />
); }; export default AddAddressModal; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/modals/changepasswordmodal/container/ChangePasswordModalContainer.tsx ================================================ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { ChangePasswordFields } from "../../../../constants"; import ApiError from "../../../../services/ApiError"; import AuthService from "../../../../services/auth/AuthService"; import FeedbackModal from "../../feedbackmodal/presentation/FeedbackModal"; import ChangePasswordModal from "../presentation/ChangePasswordModal"; interface ChangePasswordModalContainerProps { hideModal(): void; } const ChangePasswordModalContainer = ( props: ChangePasswordModalContainerProps ) => { const { hideModal } = props; const { t } = useTranslation(); /* Flag for whether password is changed or not */ const [isPasswordChanged, setIsPasswordChanged] = useState(false); /* For loading spinner */ const [isPasswordChangeInProgress, setIsPasswordChangeInProgress] = useState(false); /* To hold errors when calling the change password API */ const [errorChangingPassword, setErrorChangingPassword] = useState(""); /* Change password handler */ const changePasswordHandler = async (fields: ChangePasswordFields) => { /* Hide error message */ setErrorChangingPassword(""); /* Show loading spinner */ setIsPasswordChangeInProgress(true); /* API Call */ const response = await AuthService.changePassword(fields); /* Hide loading spinner */ setIsPasswordChangeInProgress(false); /* Error */ if (response instanceof ApiError) { setErrorChangingPassword( response.errorResponse?.message || response.errorMessage ); } else { /* Success */ setIsPasswordChanged(true); } }; return ( <> {isPasswordChanged ? ( ) : ( )} ); }; export default ChangePasswordModalContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/modals/changepasswordmodal/presentation/ChangePasswordModal.tsx ================================================ import { useTranslation } from "react-i18next"; import Modal from "../../../basic/Modal"; import Button from "../../../basic/Button"; import { ButtonTypes, ChangePasswordFields } from "../../../../constants"; import Input from "../../../basic/Input"; import { useForm } from "react-hook-form"; import ErrorMessage from "../../../basic/ErrorMessage"; interface ChangePasswordModalProps { hideModal(): void; errorChangingPassword?: string; changePasswordHandler(fields: ChangePasswordFields): void; isPasswordChangeInProgress: boolean; } const ChangePasswordModal = (props: ChangePasswordModalProps) => { const { hideModal, errorChangingPassword = "", changePasswordHandler, isPasswordChangeInProgress = false, } = props; const { t } = useTranslation(); const { handleSubmit, register, watch, formState: { errors }, } = useForm(); return ( <>
{errorChangingPassword && ( )} { if (watch("newPassword") !== value) { return t("passwordsDontMatch"); } }, }, })} />
{!isPasswordChangeInProgress && ( )}
); }; export default ChangePasswordModal; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/modals/deleteaddressmodal/container/DeleteAddressModalContainer.tsx ================================================ import { useState } from "react"; import DeleteAddressModal from "../presentation/DeleteAddressModal"; import { AddressClass } from "../../../../services/address/AddressTypes"; import AddressService from "../../../../services/address/AddressService"; import ApiError from "../../../../services/ApiError"; import FeedbackModal from "../../feedbackmodal/presentation/FeedbackModal"; import { useTranslation } from "react-i18next"; interface DeleteAddressModalContainerProps { hideModal(): void; onAddressDeletedCallback?(): void; address: AddressClass; } const DeleteAddressModalContainer = ( props: DeleteAddressModalContainerProps ) => { const { hideModal, onAddressDeletedCallback, address } = props; const {t} = useTranslation(); const [isDeletionInProgress, setIsDeletionInProgress] = useState(false); const [isFeedbackModalShown, setIsFeedbackModalShown] = useState(false); const [error, setError] = useState(""); const hideModalHandler = () => { if(isFeedbackModalShown){ if(onAddressDeletedCallback){ onAddressDeletedCallback(); } hideModal(); return; } hideModal(); } const addressDeleteHandler = async () => { setIsDeletionInProgress(true); setError(""); const response = await AddressService.deleteAddress(address._id); if (!(response instanceof ApiError)) { // Success setIsFeedbackModalShown(true); } else { setError(response.errorResponse?.message || response.errorMessage); } setIsDeletionInProgress(false); }; return ( <> {isFeedbackModalShown ? ( ) : ( )} ); }; export default DeleteAddressModalContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/modals/deleteaddressmodal/presentation/DeleteAddressModal.tsx ================================================ import { useTranslation } from "react-i18next"; import Modal from "../../../basic/Modal"; import ErrorMessage from "../../../basic/ErrorMessage"; interface DeleteAddressModalProps { addressDeleteHandler(): void; isDeletionInProgress: boolean; hideModal(): void; apiError?: string; } const DeleteAddressModal = (props: DeleteAddressModalProps) => { const { addressDeleteHandler, hideModal, isDeletionInProgress = false, apiError = "", } = props; const { t } = useTranslation(); return (
{apiError && } {t("areYouSureYouWantToDeleteTheAddress")}
); }; export default DeleteAddressModal; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/modals/feedbackmodal/presentation/FeedbackModal.tsx ================================================ import { useTranslation } from "react-i18next"; import ErrorMessage from "../../../basic/ErrorMessage"; import Modal from "../../../basic/Modal"; import TickIcon from "../../../icons/TickIcon"; import { useAppSelector } from "../../../../store"; interface FeedbackModalProps { messageType: "SUCCESS" | "ERROR"; message: string; hideModal(): void; } const FeedbackModal = (props: FeedbackModalProps) => { const { messageType, message, hideModal } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); return (
{messageType === "ERROR" && ( )} {messageType === "SUCCESS" && (
{message}
)}
); }; export default FeedbackModal; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/modals/forgotpasswordmodal/container/ForgotPasswordModalContainer.tsx ================================================ import { useState } from "react"; import ForgotPasswordModal from "../presentation/ForgotPasswordModal"; import { ForgotPasswordFields } from "../../../../constants"; import AuthService from "../../../../services/auth/AuthService"; import ApiResponse from "../../../../services/ApiResponse"; import FeedbackModal from "../../feedbackmodal/presentation/FeedbackModal"; import { useTranslation } from "react-i18next"; interface ForgotPasswordModalContainerProps { hideModal(): void; } const ForgotPasswordModalContainer = ( props: ForgotPasswordModalContainerProps ) => { const { hideModal } = props; const { t } = useTranslation(); /* API Request Loader */ const [isLoading, setIsLoading] = useState(false); /* If there is any error from the API */ const [apiErrorMessage, setApiErrorMessage] = useState(""); /* email sent modal visbility */ const [isEmailSentModalShown, setIsEmailSentModalShown] = useState(false); const forgotPasswordSubmitHandler = async (fields: ForgotPasswordFields) => { setIsLoading(true); setApiErrorMessage(""); const response = await AuthService.forgotPassword(fields); setIsLoading(false); /* Success show the modal saying email sent */ if (response instanceof ApiResponse) { setIsEmailSentModalShown(true); } else { /* Setting the error message */ setApiErrorMessage( response.errorResponse?.message || response.errorMessage ); } }; return ( <> {isEmailSentModalShown ? ( ) : ( )} ); }; export default ForgotPasswordModalContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/modals/forgotpasswordmodal/presentation/ForgotPasswordModal.tsx ================================================ import { useTranslation } from "react-i18next"; import { ButtonTypes, ForgotPasswordFields, REGEX_PATTERNS, } from "../../../../constants"; import Text from "../../../basic/Text"; import Input from "../../../basic/Input"; import Modal from "../../../basic/Modal"; import { useForm } from "react-hook-form"; import Button from "../../../basic/Button"; import ErrorMessage from "../../../basic/ErrorMessage"; interface ForgotPasswordProps { forgotPasswordSubmitHandler(fields: ForgotPasswordFields): void; isLoading?: boolean; apiErrorMessage?: string; hideModal(): void; } const ForgotPasswordModal = (props: ForgotPasswordProps) => { const { forgotPasswordSubmitHandler, isLoading = false, apiErrorMessage = "", hideModal, } = props; const { t } = useTranslation(); const { register, handleSubmit, formState: { errors }, } = useForm(); return (
{t("enterYourDetailsBelow")} {apiErrorMessage && ( )}
REGEX_PATTERNS.emailPattern.test(value) || t("invalidEmailAddress"), }, })} />
{!isLoading && ( )}
); }; export default ForgotPasswordModal; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/modals/logoutmodal/container/LogoutModalContainer.tsx ================================================ import { useDispatch } from "react-redux"; import LogoutModal from "../presentation/LogoutModal"; import useCustomNavigate from "../../../../hooks/useCustomNavigate"; import { logOut } from "../../../../store/AuthSlice"; import { useState } from "react"; import ApiError from "../../../../services/ApiError"; import AuthService from "../../../../services/auth/AuthService"; interface LogoutModalContainerProps { isShown?: boolean; hideModal(): void; } const LogoutModalContainer = (props: LogoutModalContainerProps) => { const { isShown = false, hideModal } = props; /* State for loading spinner */ const [logoutInProgress, setLogoutInProgress] = useState(false); /* To store any error message when logging out */ const [errorMessage, setErrorMessage] = useState(''); const dispatch = useDispatch(); const navigate = useCustomNavigate(); const logoutUser = async () => { setLogoutInProgress(true); const response = await AuthService.logoutService(); setLogoutInProgress(false); if (response instanceof ApiError) { //Error setErrorMessage(response.errorResponse?.message || response.errorMessage) } else { dispatch(logOut()); navigate("/"); } }; return ( ); }; export default LogoutModalContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/modals/logoutmodal/presentation/LogoutModal.tsx ================================================ import { useTranslation } from "react-i18next"; import Modal from "../../../basic/Modal"; import ErrorMessage from "../../../basic/ErrorMessage"; interface LogoutModalProps { isLogoutModalShown: boolean; hideLogoutModal(): void; logoutHandler(): void; isLogoutModalButtonLoading?: boolean; errorMessage?: string } const LogoutModal = (props: LogoutModalProps) => { const { t } = useTranslation(); const { isLogoutModalShown, hideLogoutModal, logoutHandler, isLogoutModalButtonLoading = false, errorMessage = '' } = props; return ( <> {isLogoutModalShown && ( { errorMessage ? : {t("areYouSureYouWantToLogout")} } )} ); }; export default LogoutModal; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/about/container/AboutContainer.tsx ================================================ import About from "../presentation/About" const AboutContainer = () => { return ( ) } export default AboutContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/about/presentation/About.tsx ================================================ import { useTranslation } from "react-i18next"; import Text from "../../../basic/Text"; import Image from "../../../basic/Image"; import { PUBLIC_IMAGE_PATHS } from "../../../../constants"; import CompanyGuranteeListContainer from "../../companyguranteelist/container/CompanyGuranteeListContainer"; import { useAppSelector } from "../../../../store"; const About = () => { const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); return (
{t("ourStory")} Lorem ipsum dolor sit amet consectetur adipisicing elit. Doloribus quibusdam tempora praesentium illum. Lorem, ipsum dolor sit amet consectetur adipisicing elit. Et eveniet autem consectetur quidem perferendis aliquid molestias ducimus repellat animi possimus. Incidunt laudantium similique pariatur qui accusamus praesentium a odio hic?
{t("about")}
); }; export default About; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/allproductlist/container/AllProductListContainer.tsx ================================================ import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import ProductService from "../../../../services/product/ProductService"; import { Product } from "../../../../services/product/ProductTypes"; import AllProductList from "../presentation/AllProductList"; import { ProductFilterFields } from "../../../../constants"; interface AllProductListContainerProps { categoryId?: string; productNameSearched?: string; } const AllProductListContainer = (props: AllProductListContainerProps) => { const { categoryId = "", productNameSearched = "" } = props; const { t } = useTranslation(); /* Loading flag */ const [isLoading, setIsLoading] = useState(false); /* Error flag: When fetching products */ const [isError, setIsError] = useState(false); /* Products List */ const [products, setProducts] = useState([]); /* Filtered Products List */ const [filteredProducts, setFilteredProducts] = useState([]); /* Displayed Products List */ const [displayedProducts, setDisplayedProducts] = useState([]); /* Category name if categoryId is passed */ const [categoryName, setCategoryName] = useState(""); /* Flag to store when data fetching has started*/ const isFetchingDataStarted = useRef(false); const setDisplayedProductsForSearch = (data: Product[]) => { setDisplayedProducts((prev) => { /* If there is space for more products to be shown */ if (prev.length <= ProductService.defaultPageLimit) { /* slice from 0 to the page limit */ prev = [ ...prev, ...data.slice(0, ProductService.defaultPageLimit - prev.length), ]; } return prev; }); }; const fetchProducts = useCallback(async () => { /* Data fetching has started */ isFetchingDataStarted.current = true; /* Hiding error, Displaying loading spinner, Resetting products list state */ setIsError(false); setIsLoading(true); setProducts([]); setFilteredProducts([]); /* Used to set displayed products list on first response */ let isFirstResponse = true; ProductService.getAllProductsAsync((data, _, error, categoryInfo) => { if (!error) { /* If productNameIsSearched, filter data by product name */ if (productNameSearched) { data = data.filter((product) => product.name ?.toLowerCase() ?.includes(productNameSearched?.toLowerCase()) ); } /* Set overall products list */ setProducts((prev) => [...prev, ...data]); setFilteredProducts((prev) => [...prev, ...data]); /* If productNameIsSearched set displayed products list by checking page limit */ if (productNameSearched) { setDisplayedProductsForSearch(data); } /* On first response, set displayed products list, category name and hide loading spinner */ if (isFirstResponse) { if (!productNameSearched) { setDisplayedProducts(data); } if (categoryInfo) { setCategoryName(categoryInfo.name); } setIsLoading(false); } } else { setIsError(true); setProducts([]); setFilteredProducts([]); } isFirstResponse = false; }, categoryId); /* Hide Loading spinner */ setIsLoading(false); }, [categoryId, productNameSearched]); /* Load more handler */ const loadMoreHandler = () => { /** * Append to displayed products list from the end of filtered products list + the default page limit */ setDisplayedProducts((prev) => [ ...prev, ...filteredProducts.slice( prev.length, prev.length + ProductService.defaultPageLimit ), ]); }; const resetFilterHandler = () => { /* Setting filtered products list to the entire product list */ setFilteredProducts(products); /* Setting displayed products from the complete product list */ setDisplayedProducts(products.slice(0, ProductService.defaultPageLimit)); }; /* Filtering products */ const onFiltersChanged = (fields: ProductFilterFields) => { /* From the entire list of products */ const filtered = products.filter((product) => { /* Price filter */ if ( product.price >= Number(fields.price.min) && product.price <= Number(fields.price.max) ) { return true; } return false; }); /* Setting filtered product list */ setFilteredProducts(filtered); /* Displayed product list from the list of filtered products */ setDisplayedProducts(filtered.slice(0, ProductService.defaultPageLimit)); }; useEffect(() => { if (!isFetchingDataStarted.current) { fetchProducts(); } }, [fetchProducts]); return ( displayedProducts.length ? true : false } loadMore={loadMoreHandler} isLoading={isLoading} onFiltersChanged={onFiltersChanged} resetFilterHandler={resetFilterHandler} /> ); }; export default AllProductListContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/allproductlist/presentation/AllProductList.tsx ================================================ import { useTranslation } from "react-i18next"; import { Product } from "../../../../services/product/ProductTypes"; import CardContainer from "../../../business/CardContainer"; import ProductList from "../../../business/ProductList"; import ErrorMessage from "../../../basic/ErrorMessage"; import { CARD_CONTAINER_OPTION, ProductFilterFields, } from "../../../../constants"; import ProductFilters from "../../../business/ProductFilters"; import { useAppSelector } from "../../../../store"; interface AllProductListProps { products: Product[]; heading: string; error?: boolean; isLoading?: boolean; loadMoreShown: boolean; loadMore(): void; onFiltersChanged(fields: ProductFilterFields): void; resetFilterHandler(): void; } const AllProductList = (props: AllProductListProps) => { const { products, heading, error = false, isLoading = false, loadMoreShown = true, loadMore, onFiltersChanged, resetFilterHandler, } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); return (
{error && ( )}
); }; export default AllProductList; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/banner/container/BannerContainer.tsx ================================================ import { useEffect, useState } from "react"; import { DATE_TIME_FORMATS } from "../../../../constants"; import { BANNER_PROMOTION_END_DATE } from "../../../../data/applicationData"; import { getCurrentUTCTime } from "../../../../utils/dateTimeHelper"; import Banner from "../presentation/Banner"; const BannerContainer = () => { /* Current time in UTC formatted as YYYY-MM-DDTHH:mm:ss */ const [startTime, setStartTime] = useState( getCurrentUTCTime(DATE_TIME_FORMATS.standardDateWithTime) ); useEffect(() => { /* Set current UTC time as start time after every 1 second */ setInterval(() => { setStartTime(getCurrentUTCTime(DATE_TIME_FORMATS.standardDateWithTime)); }, 1000); }, []); return ( ); }; export default BannerContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/banner/presentation/Banner.tsx ================================================ import { useTranslation } from "react-i18next"; import { useAppSelector } from "../../../../store"; import Image from "../../../basic/Image"; import Timer from "../../../business/Timer"; import { PUBLIC_IMAGE_PATHS } from "../../../../constants"; interface BannerProps { isTimerShown: boolean; startTime?: string; endTime?: string; } const Banner = (props: BannerProps) => { const { isTimerShown, startTime, endTime } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); return (
{t("bannerPromotion")} {t("bannerPromotion2")}
{`${t("bannerPromotion")}
{isTimerShown && startTime && endTime && ( )}
); }; export default Banner; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/cart/container/CartContainer.tsx ================================================ import { useMemo } from "react"; import { Product } from "../../../../services/product/ProductTypes"; import { useAppDispatch, useAppSelector } from "../../../../store"; import { addOrUpdateToCartThunk, removeFromCartThunk, } from "../../../../store/CartSlice"; import Cart from "../presentation/Cart"; import FullPageLoadingSpinner from "../../../basic/FullPageLoadingSpinner"; import useCustomNavigate from "../../../../hooks/useCustomNavigate"; import { ROUTE_PATHS } from "../../../../constants"; const CartContainer = () => { const navigate = useCustomNavigate(); const userCart = useAppSelector((state) => state.cart.userCart); /* Is Add / Remove from cart in progress state from redux */ const isAddOrUpdateToCartInProgress = useAppSelector( (state) => state.cart.isAddOrUpdateToCartInProgress ); const isRemoveFromCartInProgress = useAppSelector( (state) => state.cart.isRemoveFromCartInProgress ); /* If add / update / remove from cart is in progress: Show the loading spinner */ const isLoading = useMemo(() => { if (isAddOrUpdateToCartInProgress || isRemoveFromCartInProgress) { return true; } return false; }, [isAddOrUpdateToCartInProgress, isRemoveFromCartInProgress]); const dispatch = useAppDispatch(); /* On change of quantity */ const onQuantityChanged = (product: Product, quantity: number) => { dispatch( addOrUpdateToCartThunk({ productId: product._id, quantity: quantity }) ); }; /* Remove from cart */ const removeFromCart = (product: Product) => { dispatch(removeFromCartThunk({ productId: product._id })); }; /* Navigate to checkout */ const checkoutClickHandler = () => { navigate(ROUTE_PATHS.checkout, false, {isFromCartPage: true}); }; return ( <> {isLoading && } ); }; export default CartContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/cart/presentation/Cart.tsx ================================================ import { UserCart } from "../../../../services/cart/CartTypes"; import { Product } from "../../../../services/product/ProductTypes"; import { useAppSelector } from "../../../../store"; import CartItemList from "../../../business/CartItemList"; import CartSummary from "../../../business/CartSummary"; interface CartProps { cart: UserCart | null; onQuantityChanged(product: Product, quantity: number): void; removeFromCart(product: Product): void; checkoutClickHandler(): void; } const Cart = (props: CartProps) => { const { cart, onQuantityChanged, removeFromCart, checkoutClickHandler } = props; const isRTL = useAppSelector(state => state.language.isRTL); return (
{cart && (
)}
); }; export default Cart; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/categorylist/container/CategoryListContainer.tsx ================================================ import { useEffect, useState } from "react"; import CategoryList from "../presentation/CategoryList"; import { Category } from "../../../../services/category/CategoryTypes"; import CategoryService from "../../../../services/category/CategoryService"; import { createSearchParams } from "react-router-dom"; import { QUERY_PARAMS, ROUTE_PATHS } from "../../../../constants"; import useCustomNavigate from "../../../../hooks/useCustomNavigate"; const CategoryListContainer = () => { const navigate = useCustomNavigate(); /* List of all categories */ const [categories, setCategories] = useState([]); /* To know if an error has occurred when fetching categories */ const [isError, setIsError] = useState(false); /* Fetch All Categories Asynchronously */ const fetchAllCategories = () => { CategoryService.getAllCategoriesAsync((data, _, error) => { if (!error) { setCategories((prev) => [...prev, ...data]); } else { console.error("Error -- fetchAllCategories()", error); setIsError(true); } }); }; /* navigate to /products?category= */ const categoryClickHandler = (category: Category) => { navigate({ pathname: ROUTE_PATHS.products, search: createSearchParams({ [QUERY_PARAMS.category]: category._id, }).toString(), }); }; /* Initial Render */ useEffect(() => { fetchAllCategories(); }, []); return ( ); }; export default CategoryListContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/categorylist/presentation/CategoryList.tsx ================================================ import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { CARD_CONTAINER_OPTION } from "../../../../constants"; import { Category } from "../../../../services/category/CategoryTypes"; import { useAppSelector } from "../../../../store"; import ErrorMessage from "../../../basic/ErrorMessage"; import CardContainer from "../../../business/CardContainer"; import CategoryCard from "../../../business/CategoryCard"; interface CategoryList { categories: Category[]; error: boolean; categoryClickHandler(category: Category) : void; } const CategoryList = (props: CategoryList) => { const { categories, error, categoryClickHandler } = props; const { t } = useTranslation(); /* Category Card Container Reference */ const categoryContainerRef = useRef(null); const isRTL = useAppSelector((state) => state.language.isRTL); return ( { error ? :
{categories.map((category) => (
))}
}
); }; export default CategoryList; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/checkout/container/CheckoutContainer.tsx ================================================ import { useCallback, useEffect, useState } from "react"; import Checkout from "../presentation/Checkout"; import { AddressClass } from "../../../../services/address/AddressTypes"; import AddressService from "../../../../services/address/AddressService"; import { useAppDispatch, useAppSelector } from "../../../../store"; import { CouponClass } from "../../../../services/coupon/CouponTypes"; import CouponService from "../../../../services/coupon/CouponService"; import { CheckoutApplyCouponCodeFields, TOAST_MESSAGE_TYPES, } from "../../../../constants"; import ApiError from "../../../../services/ApiError"; import { updateUserCart } from "../../../../store/CartSlice"; import { postMessageAction } from "../../../../store/ToastMessageSlice"; import { useTranslation } from "react-i18next"; const CheckoutContainer = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); /* Addresses stored by the user */ const [userAddresses, setUserAddresses] = useState>([]); /* Coupons List */ const [couponsAvailableToUser, setCouponsAvailableToUser] = useState< Array >([]); /* Loading state when applying a coupon or removing a coupon */ const [isUpdatingCouponInProgress, setIsUpdatingCouponInProgress] = useState(false); /* User's cart */ const userCart = useAppSelector((state) => state.cart.userCart); /* Fetching all the user's stored addresses asynchronously */ const fetchUserAddresses = useCallback(() => { setUserAddresses([]); AddressService.getAllAddressesAsync((data, _, error) => { if (data) { setUserAddresses((prev) => [...prev, ...data]); } else if (error) { // Error } }); }, []); /* Fetching all the coupons available to the user asynchronously */ const fetchUserCoupons = useCallback(() => { setCouponsAvailableToUser([]); CouponService.getAllCouponsAvailableToUserAsync((data, _, error) => { if (data) { setCouponsAvailableToUser((prev) => [...prev, ...data]); } else if (error) { // Error Here } }); }, []); /* Apply Coupon */ const applyCouponCodeHandler = async ( data: CheckoutApplyCouponCodeFields ) => { setIsUpdatingCouponInProgress(true); const response = await CouponService.applyCouponCode(data.couponCode); setIsUpdatingCouponInProgress(false); /* Success */ if (!(response instanceof ApiError)) { dispatch(updateUserCart(response)); dispatch( postMessageAction({ type: TOAST_MESSAGE_TYPES.success, message: t("couponCodeAppliedSuccessfully"), }) ); } else { // Error dispatch( postMessageAction({ type: TOAST_MESSAGE_TYPES.error, message: response.errorResponse?.message || response.errorMessage, }) ); } }; /* Remove applied coupon */ const removeCouponCodeHandler = async () => { if (userCart?.coupon.couponCode) { setIsUpdatingCouponInProgress(true); const response = await CouponService.removeCouponCode( userCart?.coupon.couponCode ); setIsUpdatingCouponInProgress(false); /* Success */ if (!(response instanceof ApiError)) { dispatch(updateUserCart(response)); dispatch( postMessageAction({ type: TOAST_MESSAGE_TYPES.success, message: t("couponCodeRemovedSuccessfully"), }) ); } else { // Error dispatch( postMessageAction({ type: TOAST_MESSAGE_TYPES.error, message: response.errorResponse?.message || response.errorMessage, }) ); } } }; useEffect(() => { fetchUserAddresses(); }, [fetchUserAddresses]); useEffect(() => { fetchUserCoupons(); }, [fetchUserCoupons]); return ( <> {userCart && ( )} ); }; export default CheckoutContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/checkout/presentation/Checkout.tsx ================================================ import { useEffect, useState } from "react"; import { AddressClass } from "../../../../services/address/AddressTypes"; import RadioButtons from "../../../basic/RadioButtons"; import { ButtonTypes, CheckoutApplyCouponCodeFields, CheckoutFormFields, RADIO_BUTTON_TYPE, } from "../../../../constants"; import AddressCard from "../../../business/AddressCard"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import Button from "../../../basic/Button"; import AddIcon from "../../../icons/AddIcon"; import AddAddressModalContainer from "../../../modals/addaddressmodal/container/AddAddressModalContainer"; import { UserCart } from "../../../../services/cart/CartTypes"; import InvoiceAmountSummary from "../../../business/InvoiceAmountSummary"; import { CouponClass } from "../../../../services/coupon/CouponTypes"; import CouponCardList from "../../../business/CouponCardList"; import Input from "../../../basic/Input"; import { useAppSelector } from "../../../../store"; import Text from "../../../basic/Text"; import ErrorMessage from "../../../basic/ErrorMessage"; import Payment from "../../../business/Payment"; import { DEFAULT_CURRENCY } from "../../../../data/applicationData"; interface CheckoutProps { userAddresses: Array; couponsAvailableToUser: Array; userCart: UserCart; isUpdatingCouponInProgress: boolean; refreshUserAddresses(): void; applyCouponCodeHandler(data: CheckoutApplyCouponCodeFields): void; removeCouponCodeHandler(): void; } const Checkout = (props: CheckoutProps) => { const { userAddresses = [], couponsAvailableToUser = [], userCart, refreshUserAddresses, applyCouponCodeHandler, removeCouponCodeHandler, isUpdatingCouponInProgress = false, } = props; const isRTL = useAppSelector((state) => state.language.isRTL); const { t } = useTranslation(); /* To convert User Addresses, to types which can be passed to radio buttons component */ const [addressRadioButtons, setAddressRadioButtons] = useState< Array> >([]); /* If add address modal is shown */ const [isAddressModalShown, setIsAddressModalShown] = useState(false); const { watch: watchAddress, reset, control } = useForm(); const { handleSubmit: couponHandleSubmit, register: couponCodeRegister, formState: { errors: couponCodeErrors }, } = useForm(); useEffect(() => { /* Creating Radio Button Type Array */ const tempAddressRadioButton: Array> = []; userAddresses.forEach((address) => { tempAddressRadioButton.push({ id: address._id, isDefaultSelected: false, customElement: ( ), data: address, }); }); setAddressRadioButtons(tempAddressRadioButton); reset(); /* Reset Address Form Field */ }, [userAddresses, refreshUserAddresses, reset]); return ( <> {isAddressModalShown && ( setIsAddressModalShown(false)} onAddressAddedOrUpdatedCallback={refreshUserAddresses} /> )}
{t("selectAddress")} ( )} />
{!userCart?.coupon?.couponCode ? (
) : ( )} {!watchAddress("address") && ( )}
); }; export default Checkout; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/companyguranteelist/container/CompanyGuranteeListContainer.tsx ================================================ import { COMPANY_GURANTEE_LIST } from "../../../../data/applicationData"; import CompanyGuranteeList from "../presentation/CompanyGuranteeList" const CompanyGuranteeListContainer = () => { return ( ) } export default CompanyGuranteeListContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/companyguranteelist/presentation/CompanyGuranteeList.tsx ================================================ import { COMPANY_GURANTEE } from "../../../../constants"; import { useAppSelector } from "../../../../store"; import CompanyGurantee from "../../../business/CompanyGurantee"; interface CompanyGuranteeListProps { gurantees: COMPANY_GURANTEE[]; } const CompanyGuranteeList = (props: CompanyGuranteeListProps) => { const { gurantees } = props; const isRTL = useAppSelector((state) => state.language.isRTL) return (
{gurantees.map((gurantee) => (
))}
); }; export default CompanyGuranteeList; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/editaddresses/container/EditAddressesContainer.tsx ================================================ import { useCallback, useEffect, useState } from "react"; import AddressService from "../../../../services/address/AddressService"; import { AddressClass } from "../../../../services/address/AddressTypes"; import EditAddresses from "../presentation/EditAddresses"; import ErrorMessage from "../../../basic/ErrorMessage"; import { useTranslation } from "react-i18next"; const EditAddressesContainer = () => { const { t } = useTranslation(); /* User's addresses */ const [addressList, setAddressList] = useState([]); /* For error while fetching addresses */ const [errorFetchingAddresses, setErrorFetchingAddresses] = useState(false); const fetchAddresses = useCallback(() => { /* Resetting address list */ setAddressList([]); /* Hiding the error message */ setErrorFetchingAddresses(false); AddressService.getAllAddressesAsync((data, _, error) => { if (!error) { /* Appending items to address list state */ setAddressList((prev) => [...prev, ...data]); } else { /* In case of error, showing the error message */ setErrorFetchingAddresses(true); } }); }, []); /* Initial Render */ useEffect(() => { fetchAddresses(); }, [fetchAddresses]); return ( <> {errorFetchingAddresses ? ( ) : ( )} ); }; export default EditAddressesContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/editaddresses/presentation/EditAddresses.tsx ================================================ import { useState } from "react"; import { AddressClass } from "../../../../services/address/AddressTypes"; import Button from "../../../basic/Button"; import AddressCardList from "../../../business/AddressCardList"; import AddIcon from "../../../icons/AddIcon"; import AddAddressModalContainer from "../../../modals/addaddressmodal/container/AddAddressModalContainer"; interface EditAddressesProps { addressList: AddressClass[]; errorFetchingAddresses?: boolean; refetchAddresses(): void; } const EditAddresses = (props: EditAddressesProps) => { const { addressList, refetchAddresses } = props; /* Visibility for add address modal */ const [isAddAdressModalShown, setIsAddAddressModalShown] = useState(false); /* Toggling add address modal */ const toggleAddAddressModal = () => { setIsAddAddressModalShown((prev) => !prev); }; return (
{isAddAdressModalShown && ( )}
); }; export default EditAddresses; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/editprofile/container/EditProfileContainer.tsx ================================================ import { useCallback, useEffect, useMemo, useState } from "react"; import ApiError from "../../../../services/ApiError"; import ProfileService from "../../../../services/profile/ProfileService"; import EditProfile from "../presentation/EditProfile"; import { ProfileFormFields, TOAST_MESSAGE_TYPES } from "../../../../constants"; import ErrorMessage from "../../../basic/ErrorMessage"; import { useAppDispatch, useAppSelector } from "../../../../store"; import { postMessageAction } from "../../../../store/ToastMessageSlice"; import { useTranslation } from "react-i18next"; import { LOGIN_TYPES } from "../../../../services/auth/AuthTypes"; const EditProfileContainer = () => { const dispatch = useAppDispatch(); const {t} = useTranslation(); const loggedInUser = useAppSelector((state) => state.auth.userDetails); const [currentProfile, setCurrentProfile] = useState(); const [errorFetchingProfile, setErrorFetchingProfile] = useState(""); const [updateInProgress, setUpdateInProgress] = useState(false); const isChangePasswordOptionVisible = useMemo(() => { if(loggedInUser?.loginType !== LOGIN_TYPES.emailPassword){ return false; } return true; }, [loggedInUser]) const fetchUserProfile = useCallback(async () => { setErrorFetchingProfile(""); const response = await ProfileService.getUserProfile(); if (!(response instanceof ApiError)) { setCurrentProfile({ firstName: response.firstName, lastName: response.lastName, countryCode: response.countryCode, phoneNumber: response.phoneNumber, }); } else { /* Error fetching profile */ setErrorFetchingProfile( response.errorResponse?.message || response.errorMessage ); } }, []); const updateProfileHandler = async (data: ProfileFormFields) => { setUpdateInProgress(true); const response = await ProfileService.updateUserProfile( data.countryCode, data.firstName, data.lastName, data.phoneNumber ); setUpdateInProgress(false); if (!(response instanceof ApiError)) { setCurrentProfile({ firstName: response.firstName, lastName: response.lastName, countryCode: response.countryCode, phoneNumber: response.phoneNumber, }); dispatch( postMessageAction({ type: TOAST_MESSAGE_TYPES.success, message: t("profileUpdatedSuccessfully"), }) ); } else { /* Error */ dispatch( postMessageAction({ type: TOAST_MESSAGE_TYPES.error, message: response.errorResponse?.message || response.errorMessage, }) ); } }; useEffect(() => { fetchUserProfile(); }, [fetchUserProfile]); return ( <> {currentProfile ? ( ) : errorFetchingProfile ? ( ) : ( <> )} ); }; export default EditProfileContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/editprofile/presentation/EditProfile.tsx ================================================ import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { ButtonTypes, LinkTypes, ProfileFormFields, REGEX_PATTERNS, } from "../../../../constants"; import { useAppSelector } from "../../../../store"; import Button from "../../../basic/Button"; import Input from "../../../basic/Input"; import Link from "../../../basic/Link"; import ChangePasswordModalContainer from "../../../modals/changepasswordmodal/container/ChangePasswordModalContainer"; interface EditProfileProps { currentProfile: ProfileFormFields; updateProfileHandler(data: ProfileFormFields): void; updateInProgress: boolean; isChangePasswordOptionVisible: boolean; } const EditProfile = (props: EditProfileProps) => { const { currentProfile, updateProfileHandler, updateInProgress, isChangePasswordOptionVisible = true, } = props; const { t } = useTranslation(); const isRTL = useAppSelector((state) => state.language.isRTL); /* Change password modal visibility */ const [isChangePasswordModalShown, setIsChangePasswordModalShown] = useState(false); /* Whether profile values are updated */ const [isValuesUpdated, setIsValuesUpdated] = useState(false); const { handleSubmit, register, setValue, watch, formState: { errors }, } = useForm(); const toggleChangePasswordModal = () => { setIsChangePasswordModalShown((prev) => !prev); }; useEffect(() => { let key: keyof typeof currentProfile; /* Whenever current profile prop changes, set the input values */ for (key in currentProfile) { setValue(key, currentProfile[key]); } }, [currentProfile, setValue]); useEffect(() => { /* Checking on change of input, whether the values are updated or they remain same */ const subscribe = watch((value, { name }) => { if (name && currentProfile[name] !== value[name]) { setIsValuesUpdated(true); } else { setIsValuesUpdated(false); } }); /* Setting to false as currentProfile has been updated here */ setIsValuesUpdated(false); return () => { subscribe.unsubscribe(); }; }, [currentProfile, watch]); return ( <> {isChangePasswordModalShown && ( )}
{t("editYourProfile")}
REGEX_PATTERNS.countryCodePattern.test(value) || t("invalidCountryCode"), }, })} /> REGEX_PATTERNS.phoneNumberPattern.test(value) || t("invalidPhoneNumber"), }, })} />
{isChangePasswordOptionVisible && ( )}
); }; export default EditProfile; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/exploreproductlist/container/ExploreProductListContainer.tsx ================================================ import { useEffect, useState } from "react"; import ExploreProductList from "../presentation/ExploreProductList"; import { Product } from "../../../../services/product/ProductTypes"; import ProductService from "../../../../services/product/ProductService"; import { EXPLORE_PRODUCTS_COUNT } from "../../../../data/applicationData"; import { ROUTE_PATHS } from "../../../../constants"; import useCustomNavigate from "../../../../hooks/useCustomNavigate"; const ExploreProductListContainer = () => { const navigate = useCustomNavigate(); /* Products List */ const [products, setProducts] = useState([]); /* True / False: Representing any error when fetching the products list */ const [isError, setIsError] = useState(false); const fetchProducts = async () => { const response = await ProductService.getTopProducts(EXPLORE_PRODUCTS_COUNT); if (response instanceof Array) { setProducts(response); } else { // Error here setIsError(true); } }; /* navigate to /products */ const viewAllClickHandler = () => { navigate(ROUTE_PATHS.products); } useEffect(() => { fetchProducts(); }, []); return ; }; export default ExploreProductListContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/exploreproductlist/presentation/ExploreProductList.tsx ================================================ import { useTranslation } from "react-i18next"; import CardContainer from "../../../business/CardContainer"; import { CARD_CONTAINER_OPTION } from "../../../../constants"; import { Product } from "../../../../services/product/ProductTypes"; import ErrorMessage from "../../../basic/ErrorMessage"; import ProductList from "../../../business/ProductList"; interface ExploreProductListProps { products: Product[]; error?: boolean; viewAllClickHandler(): void; } const ExploreProductList = (props: ExploreProductListProps) => { const { products, error = false, viewAllClickHandler } = props; const { t } = useTranslation(); return ( {error ? ( ) : ( )} ); }; export default ExploreProductList; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/featuredproductlist/container/FeaturedProductListContainer.tsx ================================================ import { useEffect, useState } from "react"; import { Product } from "../../../../services/product/ProductTypes"; import ProductService from "../../../../services/product/ProductService"; import FeaturedProductList from "../presentation/FeaturedProductList"; import { FEATURED_PRODUCTS_COUNT } from "../../../../data/applicationData"; import { ROUTE_PATHS } from "../../../../constants"; import useCustomNavigate from "../../../../hooks/useCustomNavigate"; const FeaturedProductListContainer = () => { const navigate = useCustomNavigate(); /* List of products */ const [products, setProducts] = useState([]); /* True/False: Representing any error when fetching featured products */ const [isError, setIsError] = useState(false); /* Fetch featured products */ const fetchFeaturedProducts = async () => { const response = await ProductService.getTopOnSaleProducts(FEATURED_PRODUCTS_COUNT); if (Array.isArray(response)) { setProducts(response); } else { // Error setIsError(true); } }; /* navigate to /products */ const viewAllClickHandler = () => { navigate(ROUTE_PATHS.products); } /* Initial Render */ useEffect(() => { fetchFeaturedProducts(); }, []); return ; }; export default FeaturedProductListContainer; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/featuredproductlist/presentation/FeaturedProductList.tsx ================================================ import { useTranslation } from "react-i18next"; import CardContainer from "../../../business/CardContainer"; import { CARD_CONTAINER_OPTION } from "../../../../constants"; import { Product } from "../../../../services/product/ProductTypes"; import ErrorMessage from "../../../basic/ErrorMessage"; import ProductList from "../../../business/ProductList"; interface FeaturedProductListProps { products: Product[]; error?: boolean; viewAllClickHandler(): void; } const FeaturedProductList = (props: FeaturedProductListProps) => { const { products, error = false, viewAllClickHandler } = props; const { t } = useTranslation(); return ( {error ? ( ) : ( )} ); }; export default FeaturedProductList; ================================================ FILE: examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/footer/container/FooterContainer.tsx ================================================ import Footer from "../presentation/Footer" const FooterContainer = () => { return (