├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .firebaserc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode └── settings.json ├── 404.html ├── LICENSE ├── README.md ├── database.rules.json ├── firebase.json ├── firestore-debug.log ├── firestore.indexes.json ├── firestore.rules ├── functions ├── .eslintrc.json ├── .gitignore ├── index.js └── package.json ├── index.html ├── jest.config.json ├── jsconfig.json ├── package.json ├── public └── index.html ├── src ├── App.jsx ├── components │ ├── basket │ │ ├── Basket.jsx │ │ ├── BasketItem.jsx │ │ ├── BasketItemControl.jsx │ │ ├── BasketToggle.jsx │ │ └── index.js │ ├── common │ │ ├── AdminNavigation.jsx │ │ ├── AdminSidePanel.jsx │ │ ├── Badge.jsx │ │ ├── Boundary.jsx │ │ ├── ColorChooser.jsx │ │ ├── Filters.jsx │ │ ├── FiltersToggle.jsx │ │ ├── Footer.jsx │ │ ├── ImageLoader.jsx │ │ ├── MessageDisplay.jsx │ │ ├── MobileNavigation.jsx │ │ ├── Modal.jsx │ │ ├── Navigation.jsx │ │ ├── Preloader.jsx │ │ ├── PriceRange │ │ │ ├── Handle.jsx │ │ │ ├── SliderRail.jsx │ │ │ ├── Tick.jsx │ │ │ ├── TooltipRail.jsx │ │ │ ├── Track.jsx │ │ │ └── index.jsx │ │ ├── SearchBar.jsx │ │ ├── SocialLogin.jsx │ │ └── index.js │ ├── formik │ │ ├── CustomColorInput.jsx │ │ ├── CustomCreatableSelect.jsx │ │ ├── CustomInput.jsx │ │ ├── CustomMobileInput.jsx │ │ ├── CustomTextarea.jsx │ │ └── index.js │ └── product │ │ ├── ProductAppliedFilters.jsx │ │ ├── ProductFeatured.jsx │ │ ├── ProductGrid.jsx │ │ ├── ProductItem.jsx │ │ ├── ProductList.jsx │ │ ├── ProductSearch.jsx │ │ ├── ProductShowcaseGrid.jsx │ │ └── index.js ├── constants │ ├── constants.js │ ├── index.js │ └── routes.js ├── helpers │ └── utils.js ├── hooks │ ├── index.js │ ├── useBasket.js │ ├── useDidMount.js │ ├── useDocumentTitle.js │ ├── useFeaturedProducts.js │ ├── useFileHandler.js │ ├── useModal.js │ ├── useProduct.js │ ├── useRecommendedProducts.js │ └── useScrollTop.js ├── images │ ├── banner-girl-1.png │ ├── banner-girl.png │ ├── banner-guy.png │ ├── creditcards.png │ ├── defaultAvatar.jpg │ ├── defaultBanner.jpg │ ├── logo-full.png │ ├── logo-wordmark.png │ └── square.jpg ├── index.jsx ├── redux │ ├── actions │ │ ├── authActions.js │ │ ├── basketActions.js │ │ ├── checkoutActions.js │ │ ├── filterActions.js │ │ ├── index.js │ │ ├── miscActions.js │ │ ├── productActions.js │ │ ├── profileActions.js │ │ └── userActions.js │ ├── reducers │ │ ├── authReducer.js │ │ ├── basketReducer.js │ │ ├── checkoutReducer.js │ │ ├── filterReducer.js │ │ ├── index.js │ │ ├── miscReducer.js │ │ ├── productReducer.js │ │ ├── profileReducer.js │ │ └── userReducer.js │ ├── sagas │ │ ├── authSaga.js │ │ ├── productSaga.js │ │ ├── profileSaga.js │ │ └── rootSaga.js │ └── store │ │ └── store.js ├── routers │ ├── AdminRoute.jsx │ ├── AppRouter.jsx │ ├── ClientRoute.jsx │ └── PublicRoute.jsx ├── selectors │ └── selector.js ├── services │ ├── config.js │ └── firebase.js ├── styles │ ├── 1 - settings │ │ ├── _breakpoints.scss │ │ ├── _colors.scss │ │ ├── _sizes.scss │ │ ├── _typography.scss │ │ └── _zindex.scss │ ├── 2 - tools │ │ ├── _functions.scss │ │ └── _mixins.scss │ ├── 4 - elements │ │ ├── _base.scss │ │ ├── _button.scss │ │ ├── _input.scss │ │ ├── _label.scss │ │ ├── _link.scss │ │ ├── _select.scss │ │ └── _textarea.scss │ ├── 5 - components │ │ ├── 404 │ │ │ └── _page-not-found.scss │ │ ├── _auth.scss │ │ ├── _badge.scss │ │ ├── _banner.scss │ │ ├── _basket.scss │ │ ├── _circular-progress.scss │ │ ├── _color-chooser.scss │ │ ├── _filter.scss │ │ ├── _footer.scss │ │ ├── _home.scss │ │ ├── _icons.scss │ │ ├── _modal.scss │ │ ├── _navigation.scss │ │ ├── _pill.scss │ │ ├── _preloader.scss │ │ ├── _pricerange.scss │ │ ├── _product.scss │ │ ├── _searchbar.scss │ │ ├── _sidebar.scss │ │ ├── _sidenavigation.scss │ │ ├── _toast.scss │ │ ├── _tooltip.scss │ │ ├── admin │ │ │ ├── _grid.scss │ │ │ └── _product.scss │ │ ├── checkout │ │ │ └── _checkout.scss │ │ ├── mobile │ │ │ ├── _bottom-navigation.scss │ │ │ └── _mobile-navigation.scss │ │ └── profile │ │ │ ├── _editprofile.scss │ │ │ ├── _user-nav.scss │ │ │ ├── _user-profile.scss │ │ │ └── _user-tab.scss │ ├── 6 - utils │ │ ├── _animation.scss │ │ ├── _state.scss │ │ └── _utils.scss │ └── style.scss ├── sw-src.js └── views │ ├── account │ ├── components │ │ ├── UserAccountTab.jsx │ │ ├── UserAvatar.jsx │ │ ├── UserOrdersTab.jsx │ │ ├── UserTab.jsx │ │ └── UserWishListTab.jsx │ ├── edit_account │ │ ├── ConfirmModal.jsx │ │ ├── EditForm.jsx │ │ └── index.jsx │ └── user_account │ │ └── index.jsx │ ├── admin │ ├── add_product │ │ └── index.jsx │ ├── components │ │ ├── ProductForm.jsx │ │ ├── ProductItem.jsx │ │ ├── ProductsNavbar.jsx │ │ ├── ProductsTable.jsx │ │ └── index.js │ ├── dashboard │ │ └── index.jsx │ ├── edit_product │ │ └── index.jsx │ └── products │ │ └── index.jsx │ ├── auth │ ├── forgot_password │ │ └── index.jsx │ ├── signin │ │ └── index.jsx │ └── signup │ │ └── index.jsx │ ├── checkout │ ├── components │ │ ├── StepTracker.jsx │ │ └── index.js │ ├── hoc │ │ └── withCheckout.jsx │ ├── step1 │ │ └── index.jsx │ ├── step2 │ │ ├── ShippingForm.jsx │ │ ├── ShippingTotal.jsx │ │ └── index.jsx │ └── step3 │ │ ├── CreditPayment.jsx │ │ ├── PayPalPayment.jsx │ │ ├── Total.jsx │ │ └── index.jsx │ ├── error │ ├── Error.jsx │ ├── NoInternetPage.jsx │ └── PageNotFound.jsx │ ├── featured │ └── index.jsx │ ├── home │ └── index.jsx │ ├── index.js │ ├── recommended │ └── index.jsx │ ├── search │ └── index.jsx │ ├── shop │ └── index.jsx │ └── view_product │ └── index.jsx ├── static ├── banner.jpg ├── creditcards.png ├── favicon.png ├── logo-full.png ├── logo-wordmark.png ├── profile.jpg ├── salt-image-1.png ├── salt-image-10.png ├── salt-image-2.png ├── salt-image-3.png ├── salt-image-4.png ├── salt-image-5.png ├── salt-image-7.png ├── screeny1.png ├── screeny2.png ├── screeny3.png ├── screeny4.png ├── screeny5.png ├── screeny6.png ├── screeny7.png ├── screeny8.png └── screeny9.png ├── storage.rules ├── test ├── components │ ├── App.test.js │ └── __snapshots__ │ │ └── App.test.js.snap └── setup.js ├── vite.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-syntax-object-rest-spread", 9 | "@babel/plugin-syntax-dynamic-import" 10 | ] 11 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /webpack/ 2 | /*.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "plugin:react/recommended", 8 | "airbnb" 9 | ], 10 | "parser": "babel-eslint", 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": 12, 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "react" 20 | ], 21 | "rules": { 22 | "no-multiple-empty-lines": [ 23 | "error", 24 | { 25 | "max": 2, 26 | "maxEOF": 1 27 | } 28 | ], 29 | "comma-dangle": [ 30 | "error", 31 | { 32 | "arrays": "never", 33 | "objects": "never", 34 | "imports": "never", 35 | "exports": "never", 36 | "functions": "ignore" 37 | } 38 | ], 39 | "no-console": 0 40 | }, 41 | "settings": { 42 | "import/resolver": { 43 | "node": { 44 | "paths": [ 45 | "src" 46 | ], 47 | "extensions": [ 48 | ".js", 49 | ".jsx", 50 | ".ts", 51 | ".tsx" 52 | ] 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "salinaka-ecommerce" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Firebase Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | name: Build and Deploy to Firebase Hosting 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@master 15 | - name: Install Dependencies 16 | run: yarn install 17 | - name: Build 18 | run: yarn build 19 | env: 20 | FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} 21 | FIREBASE_AUTH_DOMAIN: ${{ secrets.FIREBASE_AUTH_DOMAIN }} 22 | FIREBASE_DB_URL: ${{ secrets.FIREBASE_DB_URL }} 23 | FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }} 24 | FIREBASE_STORAGE_BUCKET: ${{ secrets.FIREBASE_STORAGE_BUCKET }} 25 | FIREBASE_MSG_SENDER_ID: ${{ secrets.FIREBASE_MSG_SENDER_ID }} 26 | FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }} 27 | - name: Deploy to Firebase 28 | uses: lowply/deploy-firebase@v0.0.2 29 | env: 30 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} 31 | FIREBASE_PROJECT: ${{ secrets.FIREBASE_PROJECT_ID }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | .env.prod 4 | .env.dev 5 | dist/ 6 | .firebase/ 7 | package-lock.json 8 | debug.log -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "editor.tabSize": 2 4 | } -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Salinaka - React Ecommerce Store 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salinaka | E-commerce react app 2 | Simple ecommerce react js app with firebase [typescript]. 3 | ![Firebase Deploy](https://github.com/jgudo/ecommerce-react/workflows/Firebase%20Deploy/badge.svg) 4 | 5 | ### [Live demo](https://salinaka-ecommerce.web.app/) 6 | 7 | ![Salinaka screenshot](https://raw.githubusercontent.com/jgudo/ecommerce-react/master/static/screeny1.png) 8 | ![Salinaka screenshot](https://raw.githubusercontent.com/jgudo/ecommerce-react/master/static/screeny2.png) 9 | ![Salinaka screenshot](https://raw.githubusercontent.com/jgudo/ecommerce-react/master/static/screeny3.png) 10 | ![Salinaka screenshot](https://raw.githubusercontent.com/jgudo/ecommerce-react/master/static/screeny7.png) 11 | 12 | ## Run Locally 13 | ### 1. Install Dependencies 14 | ```sh 15 | $ yarn install 16 | ``` 17 | 18 | ### 2. Create a new firebase project 19 | Login to your google account and create a new firebase project [here](https://console.firebase.google.com/u/0/) 20 | 21 | Create an `.env` file and add the following variables. 22 | 23 | ``` 24 | // SAMPLE CONFIG .env, you should put the actual config details found on your project settings 25 | 26 | VITE_FIREBASE_API_KEY=AIzaKJgkjhSdfSgkjhdkKJdkjowf 27 | VITE_FIREBASE_AUTH_DOMAIN=yourauthdomin.firebaseapp.com 28 | VITE_FIREBASE_DB_URL=https://yourdburl.firebaseio.com 29 | VITE_FIREBASE_PROJECT_ID=yourproject-id 30 | VITE_FIREBASE_STORAGE_BUCKET=yourstoragebucket.appspot.com 31 | VITE_FIREBASE_MSG_SENDER_ID=43597918523958 32 | VITE_FIREBASE_APP_ID=234598789798798fg3-034 33 | 34 | ``` 35 | 36 | After setting up necessary configuration, 37 | create a **Database** and choose **Cloud Firestore** and start in test mode 38 | 39 | ### 3. Run development server 40 | ```sh 41 | $ yarn dev 42 | ``` 43 | 44 | --- 45 | 46 | ## Build the project 47 | ```sh 48 | $ yarn build 49 | ``` 50 | 51 | ## How to add products or perform CRUD operations for Admin 52 | 1. Navigate to your site to `/signup` 53 | 2. Create an account for yourself 54 | 3. Go to your firestore collection `users collection` and edit the account you've just created. Change the role from `USER` to `ADMIN`. 55 | 4. Reload or sigin again to see the changes. 56 | 57 | **Firebase Admin to be integrated soon** 58 | 59 | ## Features 60 | 61 | * Admin CRUD operations 62 | * Firebase authentication 63 | * Firebase auth provider authentication 64 | * Account creation and edit 65 | 66 | -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": false, 4 | ".write": "auth !== null", 5 | "users": { 6 | "$user_id": { 7 | ".read": "$user_id === auth.uid", 8 | ".write": "$user_id === auth.uid", 9 | "fullname": { 10 | ".validate": "newData.isString()" 11 | }, 12 | "email": { 13 | ".validate": "newData.isString()" 14 | }, 15 | "address": { 16 | ".validate": "newData.isString()" 17 | }, 18 | "mobile": { 19 | ".validate": "newData.isString()" 20 | }, 21 | "avatar": { 22 | ".validate": "newData.isString()" 23 | }, 24 | "banner": { 25 | ".validate": "newData.isString()" 26 | }, 27 | "dateJoined": { 28 | ".validate": "newData.isString()" 29 | } 30 | } 31 | }, 32 | "products": { 33 | ".read": true, 34 | ".write": false, 35 | "$product_id": { 36 | "name": { 37 | ".validate": "newData.isString()" 38 | }, 39 | "description": { 40 | ".validate": "newData.isString()" 41 | }, 42 | "price": { 43 | ".validate": "newData.isNumber()" 44 | }, 45 | "brand": { 46 | ".validate": "newData.isString()" 47 | }, 48 | "image": { 49 | ".validate": "newData.isString()" 50 | }, 51 | "maxQuantity": { 52 | ".validate": "newData.isNumber()" 53 | }, 54 | "quantity": { 55 | ".validate": "newData.isNumber()" 56 | }, 57 | "dateAdded": { 58 | ".validate": "newData.isNumber()" 59 | } 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "dist", 7 | "ignore": [ 8 | "firebase.json", 9 | "**/.*", 10 | "**/node_modules/**" 11 | ] 12 | }, 13 | "emulators": { 14 | "firestore": { 15 | "port": "4000" 16 | } 17 | }, 18 | "storage": { 19 | "rules": "storage.rules" 20 | }, 21 | "firestore": { 22 | "rules": "firestore.rules", 23 | "indexes": "firestore.indexes.json" 24 | }, 25 | "functions": { 26 | "predeploy": [ 27 | "npm --prefix \"$RESOURCE_DIR\" run lint" 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /firestore-debug.log: -------------------------------------------------------------------------------- 1 | API endpoint: http://localhost:4000 2 | If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run: 3 | 4 | export FIRESTORE_EMULATOR_HOST=localhost:4000 5 | 6 | Dev App Server is now running. 7 | 8 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | 5 | // This rule allows anyone on the internet to view, edit, and delete 6 | // all data in your Firestore database. It is useful for getting 7 | // started, but it is configured to expire after 30 days because it 8 | // leaves your app open to attackers. At that time, all client 9 | // requests to your Firestore database will be denied. 10 | // 11 | // Make sure to write security rules for your app before that time, or else 12 | // your app will lose access to your Firestore database 13 | //match /{document=**} { 14 | // allow read, write: if request.time < timestamp.date(2020, 4, 21); 15 | //} 16 | 17 | match /users/{userId} { 18 | allow read, update: if request.auth.uid != null && request.auth.uid == userId; 19 | allow create: if request.auth.uid != null; 20 | } 21 | 22 | match /products/{productId} { 23 | allow create,update,delete: if request.auth.uid != null; 24 | allow read: if true; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const admin = require('firebase-admin'); 3 | 4 | admin.initializeApp(); 5 | 6 | exports.lowercaseProductName = functions.firestore.document('/products/{documentId}') 7 | .onCreate((snap, context) => { 8 | const name = snap.data().name; 9 | 10 | functions.logger.log('Lowercasing product name', context.params.documentId, name); 11 | 12 | const lowercaseName = name.toLowerCase(); 13 | 14 | return snap.ref.set({ name_lower: lowercaseName }, { merge: true }); 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint .", 6 | "serve": "firebase emulators:start --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "8" 14 | }, 15 | "main": "index.js", 16 | "dependencies": { 17 | "firebase-admin": "^9.2.0", 18 | "firebase-functions": "^3.11.0" 19 | }, 20 | "devDependencies": { 21 | "eslint": "^5.12.0", 22 | "eslint-plugin-promise": "^4.0.1", 23 | "firebase-functions-test": "^0.2.0" 24 | }, 25 | "private": true 26 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Salinaka - React Ecommerce Store 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "setupFiles": [ 3 | "raf/polyfill", 4 | "/test/setup.js" 5 | ], 6 | "snapshotSerializers": [ 7 | "enzyme-to-json/serializer" 8 | ] 9 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "baseUrl": "src", 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true 7 | }, 8 | "include": [ 9 | "./src" 10 | ] 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerce-react", 3 | "version": "1.2.0", 4 | "main": "index.js", 5 | "author": "Julius Guevarra", 6 | "keywords": [ 7 | "react-webpack", 8 | "boilerplate", 9 | "react-webpack-boilerplate", 10 | "react-boilerplate" 11 | ], 12 | "license": "MIT", 13 | "scripts": { 14 | "dev": "vite", 15 | "build": "vite build && cd dist && cp index.html 404.html", 16 | "serve": "vite preview", 17 | "test": "cross-env NODE_ENV=test jest --config=jest.config.json" 18 | }, 19 | "devDependencies": { 20 | "@vitejs/plugin-react": "^2.0.0", 21 | "copy-webpack-plugin": "^8.1.1", 22 | "cross-env": "^7.0.3", 23 | "css-loader": "^5.2.4", 24 | "dotenv": "^8.2.0", 25 | "enzyme": "^3.11.0", 26 | "enzyme-adapter-react-16": "^1.15.6", 27 | "enzyme-to-json": "^3.6.2", 28 | "eslint": "^7.25.0", 29 | "eslint-config-airbnb": "^18.2.1", 30 | "eslint-loader": "^4.0.2", 31 | "eslint-plugin-import": "^2.22.1", 32 | "eslint-plugin-jsx-a11y": "^6.4.1", 33 | "eslint-plugin-react": "^7.23.2", 34 | "eslint-plugin-react-hooks": "^4.2.0", 35 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 36 | "file-loader": "^6.2.0", 37 | "group-css-media-queries-loader": "^3.0.2", 38 | "html-webpack-plugin": "^5.3.1", 39 | "jest": "^26.6.3", 40 | "live-server": "^1.2.1", 41 | "mini-css-extract-plugin": "^1.6.0", 42 | "optimize-css-assets-webpack-plugin": "^5.0.4", 43 | "raf": "^3.4.1", 44 | "sass": "^1.53.0", 45 | "sass-loader": "^11.0.1", 46 | "uglifyjs-webpack-plugin": "^2.2.0", 47 | "url-loader": "^4.1.1", 48 | "vite": "^3.0.2", 49 | "webpack": "^5.36.2", 50 | "webpack-cli": "^4.6.0", 51 | "webpack-dev-server": "^3.11.2", 52 | "webpack-merge": "^5.7.3", 53 | "workbox-webpack-plugin": "^6.1.5" 54 | }, 55 | "dependencies": { 56 | "@ant-design/icons": "^4.6.2", 57 | "firebase": "^8.4.3", 58 | "formik": "^2.2.6", 59 | "history": "^4.10.0", 60 | "moment": "^2.29.1", 61 | "normalize.css": "^8.0.1", 62 | "prop-types": "^15.7.2", 63 | "react": "^17.0.2", 64 | "react-compound-slider": "^3.3.1", 65 | "react-dom": "^17.0.2", 66 | "react-loading-skeleton": "^2.2.0", 67 | "react-modal": "^3.13.1", 68 | "react-phone-input-2": "^2.14.0", 69 | "react-redux": "^7.2.4", 70 | "react-router-dom": "^5.2.0", 71 | "react-select": "^4.3.0", 72 | "redux": "^4.1.0", 73 | "redux-persist": "^6.0.0", 74 | "redux-saga": "^1.1.3", 75 | "redux-thunk": "^2.3.0", 76 | "uuid": "^8.3.2", 77 | "webfontloader": "^1.6.28", 78 | "yup": "^0.32.9" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/forbid-prop-types */ 2 | import { Preloader } from '@/components/common'; 3 | import PropType from 'prop-types'; 4 | import React, { StrictMode } from 'react'; 5 | import { Provider } from 'react-redux'; 6 | import { PersistGate } from 'redux-persist/integration/react'; 7 | import AppRouter from '@/routers/AppRouter'; 8 | 9 | const App = ({ store, persistor }) => ( 10 | 11 | 12 | } persistor={persistor}> 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | App.propTypes = { 20 | store: PropType.any.isRequired, 21 | persistor: PropType.any.isRequired 22 | }; 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/components/basket/BasketItemControl.jsx: -------------------------------------------------------------------------------- 1 | import { MinusOutlined, PlusOutlined } from '@ant-design/icons'; 2 | import PropType from 'prop-types'; 3 | import React from 'react'; 4 | import { useDispatch } from 'react-redux'; 5 | import { addQtyItem, minusQtyItem } from '@/redux/actions/basketActions'; 6 | 7 | const BasketItemControl = ({ product }) => { 8 | const dispatch = useDispatch(); 9 | 10 | const onAddQty = () => { 11 | if (product.quantity < product.maxQuantity) { 12 | dispatch(addQtyItem(product.id)); 13 | } 14 | }; 15 | 16 | const onMinusQty = () => { 17 | if ((product.maxQuantity >= product.quantity) && product.quantity !== 0) { 18 | dispatch(minusQtyItem(product.id)); 19 | } 20 | }; 21 | 22 | return ( 23 |
24 | 32 | 40 |
41 | ); 42 | }; 43 | 44 | BasketItemControl.propTypes = { 45 | product: PropType.shape({ 46 | id: PropType.string, 47 | name: PropType.string, 48 | brand: PropType.string, 49 | price: PropType.number, 50 | quantity: PropType.number, 51 | maxQuantity: PropType.number, 52 | description: PropType.string, 53 | keywords: PropType.arrayOf(PropType.string), 54 | selectedSize: PropType.string, 55 | selectedColor: PropType.string, 56 | imageCollection: PropType.arrayOf(PropType.string), 57 | sizes: PropType.arrayOf(PropType.number), 58 | image: PropType.string, 59 | imageUrl: PropType.string, 60 | isFeatured: PropType.bool, 61 | isRecommended: PropType.bool, 62 | availableColors: PropType.arrayOf(PropType.string) 63 | }).isRequired 64 | }; 65 | 66 | export default BasketItemControl; 67 | -------------------------------------------------------------------------------- /src/components/basket/BasketToggle.jsx: -------------------------------------------------------------------------------- 1 | import PropType from 'prop-types'; 2 | 3 | const BasketToggle = ({ children }) => { 4 | const onClickToggle = () => { 5 | if (document.body.classList.contains('is-basket-open')) { 6 | document.body.classList.remove('is-basket-open'); 7 | } else { 8 | document.body.classList.add('is-basket-open'); 9 | } 10 | }; 11 | 12 | document.addEventListener('click', (e) => { 13 | const closest = e.target.closest('.basket'); 14 | const toggle = e.target.closest('.basket-toggle'); 15 | const closeToggle = e.target.closest('.basket-item-remove'); 16 | 17 | if (!closest && document.body.classList.contains('is-basket-open') && !toggle && !closeToggle) { 18 | document.body.classList.remove('is-basket-open'); 19 | } 20 | }); 21 | 22 | return children({ onClickToggle }); 23 | }; 24 | 25 | BasketToggle.propTypes = { 26 | children: PropType.oneOfType([ 27 | PropType.arrayOf(PropType.node), 28 | PropType.func, 29 | PropType.node 30 | ]).isRequired 31 | }; 32 | 33 | export default BasketToggle; 34 | -------------------------------------------------------------------------------- /src/components/basket/index.js: -------------------------------------------------------------------------------- 1 | export { default as Basket } from './Basket'; 2 | export { default as BasketItem } from './BasketItem'; 3 | export { default as BasketItemControl } from './BasketItemControl'; 4 | export { default as BasketToggle } from './BasketToggle'; 5 | 6 | -------------------------------------------------------------------------------- /src/components/common/AdminNavigation.jsx: -------------------------------------------------------------------------------- 1 | import { ADMIN_DASHBOARD } from '@/constants/routes'; 2 | import logo from '@/images/logo-full.png'; 3 | import React from 'react'; 4 | import { useSelector } from 'react-redux'; 5 | import { Link } from 'react-router-dom'; 6 | import UserAvatar from '@/views/account/components/UserAvatar'; 7 | 8 | const AdminNavigation = () => { 9 | const { isAuthenticating, profile } = useSelector((state) => ({ 10 | isAuthenticating: state.app.isAuthenticating, 11 | profile: state.profile 12 | })); 13 | 14 | return ( 15 | 31 | ); 32 | }; 33 | 34 | export default AdminNavigation; 35 | -------------------------------------------------------------------------------- /src/components/common/AdminSidePanel.jsx: -------------------------------------------------------------------------------- 1 | import { ADMIN_PRODUCTS } from '@/constants/routes'; 2 | import React from 'react'; 3 | import { NavLink } from 'react-router-dom'; 4 | 5 | const SideNavigation = () => ( 6 | 22 | ); 23 | 24 | export default SideNavigation; 25 | -------------------------------------------------------------------------------- /src/components/common/Badge.jsx: -------------------------------------------------------------------------------- 1 | import PropType from 'prop-types'; 2 | import React from 'react'; 3 | 4 | const Badge = ({ count, children }) => ( 5 |
6 | {children} 7 | {count >= 1 && {count}} 8 |
9 | ); 10 | 11 | Badge.propTypes = { 12 | count: PropType.number.isRequired, 13 | children: PropType.oneOfType([ 14 | PropType.arrayOf(PropType.node), 15 | PropType.node 16 | ]).isRequired 17 | }; 18 | 19 | export default Badge; 20 | -------------------------------------------------------------------------------- /src/components/common/Boundary.jsx: -------------------------------------------------------------------------------- 1 | import PropType from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | class Boundary extends Component { 5 | static getDerivedStateFromError() { 6 | return { hasError: true }; 7 | } 8 | 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | hasError: false 14 | }; 15 | } 16 | 17 | 18 | componentDidCatch(error) { 19 | console.log(error); 20 | } 21 | 22 | render() { 23 | const { hasError } = this.state; 24 | const { children } = this.props; 25 | 26 | if (hasError) { 27 | return ( 28 |
29 |

:( Something went wrong.

30 |
31 | ); 32 | } 33 | 34 | return children; 35 | } 36 | } 37 | 38 | Boundary.propTypes = { 39 | children: PropType.oneOfType([ 40 | PropType.arrayOf(PropType.node), 41 | PropType.node 42 | ]).isRequired 43 | }; 44 | 45 | export default Boundary; 46 | -------------------------------------------------------------------------------- /src/components/common/ColorChooser.jsx: -------------------------------------------------------------------------------- 1 | import PropType from 'prop-types'; 2 | import React, { useState } from 'react'; 3 | 4 | const ColorChooser = ({ availableColors, onSelectedColorChange }) => { 5 | const [selectedColor, setSelectedColor] = useState(''); 6 | 7 | const setColor = (color) => { 8 | setSelectedColor(color); 9 | onSelectedColorChange(color); 10 | }; 11 | return ( 12 |
13 | {availableColors.map((color) => ( 14 |
setColor(color)} 18 | style={{ backgroundColor: color }} 19 | role="presentation" 20 | /> 21 | ))} 22 |
23 | ); 24 | }; 25 | 26 | ColorChooser.propTypes = { 27 | availableColors: PropType.arrayOf(PropType.string).isRequired, 28 | onSelectedColorChange: PropType.func.isRequired 29 | }; 30 | 31 | export default ColorChooser; 32 | -------------------------------------------------------------------------------- /src/components/common/FiltersToggle.jsx: -------------------------------------------------------------------------------- 1 | import { useModal } from '@/hooks'; 2 | import PropType from 'prop-types'; 3 | import React from 'react'; 4 | import Filters from './Filters'; 5 | import Modal from './Modal'; 6 | 7 | const FiltersToggle = ({ children }) => { 8 | const { isOpenModal, onOpenModal, onCloseModal } = useModal(); 9 | 10 | return ( 11 | <> 12 |
17 | {children} 18 |
19 | 23 |
24 | 25 |
26 | 33 |
34 | 35 | ); 36 | }; 37 | 38 | FiltersToggle.propTypes = { 39 | children: PropType.oneOfType([ 40 | PropType.arrayOf(PropType.node), 41 | PropType.node 42 | ]).isRequired 43 | }; 44 | 45 | export default FiltersToggle; 46 | -------------------------------------------------------------------------------- /src/components/common/Footer.jsx: -------------------------------------------------------------------------------- 1 | import * as Route from '@/constants/routes'; 2 | import logo from '@/images/logo-full.png'; 3 | import React from 'react'; 4 | import { useLocation } from 'react-router-dom'; 5 | 6 | const Footer = () => { 7 | const { pathname } = useLocation(); 8 | 9 | const visibleOnlyPath = [ 10 | Route.HOME, 11 | Route.SHOP 12 | ]; 13 | 14 | return !visibleOnlyPath.includes(pathname) ? null : ( 15 |
16 |
17 | 18 | 19 | Developed by 20 | {' '} 21 | JULIUS GUEVARRA 22 | 23 | 24 |
25 |
26 | Footer logo 27 |
28 | ©  29 | {new Date().getFullYear()} 30 |
31 |
32 |
33 | 34 | 35 | Fork this project   36 | HERE 37 | 38 | 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default Footer; 45 | -------------------------------------------------------------------------------- /src/components/common/ImageLoader.jsx: -------------------------------------------------------------------------------- 1 | import { LoadingOutlined } from '@ant-design/icons'; 2 | import PropType from 'prop-types'; 3 | import React, { useState } from 'react'; 4 | 5 | const ImageLoader = ({ src, alt, className }) => { 6 | const loadedImages = {}; 7 | const [loaded, setLoaded] = useState(loadedImages[src]); 8 | 9 | const onLoad = () => { 10 | loadedImages[src] = true; 11 | setLoaded(true); 12 | }; 13 | 14 | return ( 15 | <> 16 | {!loaded && ( 17 | 21 | )} 22 | {alt 28 | 29 | ); 30 | }; 31 | 32 | ImageLoader.defaultProps = { 33 | className: 'image-loader' 34 | }; 35 | 36 | ImageLoader.propTypes = { 37 | src: PropType.string.isRequired, 38 | alt: PropType.string, 39 | className: PropType.string 40 | }; 41 | 42 | export default ImageLoader; 43 | -------------------------------------------------------------------------------- /src/components/common/MessageDisplay.jsx: -------------------------------------------------------------------------------- 1 | import PropType from 'prop-types'; 2 | import React from 'react'; 3 | 4 | const MessageDisplay = ({ 5 | message, description, buttonLabel, action 6 | }) => ( 7 |
8 |

{message || 'Message'}

9 | {description && {description}} 10 |
11 | {action && ( 12 | 19 | )} 20 |
21 | ); 22 | 23 | MessageDisplay.defaultProps = { 24 | description: undefined, 25 | buttonLabel: 'Okay', 26 | action: undefined 27 | }; 28 | 29 | MessageDisplay.propTypes = { 30 | message: PropType.string.isRequired, 31 | description: PropType.string, 32 | buttonLabel: PropType.string, 33 | action: PropType.func 34 | }; 35 | 36 | export default MessageDisplay; 37 | -------------------------------------------------------------------------------- /src/components/common/MobileNavigation.jsx: -------------------------------------------------------------------------------- 1 | import { BasketToggle } from '@/components/basket'; 2 | import { HOME, SIGNIN } from '@/constants/routes'; 3 | import PropType from 'prop-types'; 4 | import React from 'react'; 5 | import { Link, useLocation } from 'react-router-dom'; 6 | import UserNav from '@/views/account/components/UserAvatar'; 7 | import Badge from './Badge'; 8 | import FiltersToggle from './FiltersToggle'; 9 | import SearchBar from './SearchBar'; 10 | 11 | const Navigation = (props) => { 12 | const { 13 | isAuthenticating, basketLength, disabledPaths, user 14 | } = props; 15 | const { pathname } = useLocation(); 16 | 17 | const onClickLink = (e) => { 18 | if (isAuthenticating) e.preventDefault(); 19 | }; 20 | 21 | return ( 22 | 76 | ); 77 | }; 78 | 79 | Navigation.propTypes = { 80 | isAuthenticating: PropType.bool.isRequired, 81 | basketLength: PropType.number.isRequired, 82 | disabledPaths: PropType.arrayOf(PropType.string).isRequired, 83 | user: PropType.oneOfType([ 84 | PropType.bool, 85 | PropType.object 86 | ]).isRequired 87 | }; 88 | 89 | export default Navigation; 90 | -------------------------------------------------------------------------------- /src/components/common/Modal.jsx: -------------------------------------------------------------------------------- 1 | import PropType from 'prop-types'; 2 | import React from 'react'; 3 | import AppModal from 'react-modal'; 4 | 5 | const Modal = ({ 6 | isOpen, 7 | onRequestClose, 8 | afterOpenModal, 9 | overrideStyle, 10 | children 11 | }) => { 12 | const defaultStyle = { 13 | content: { 14 | top: '50%', 15 | left: '50%', 16 | right: 'auto', 17 | bottom: 'auto', 18 | position: 'fixed', 19 | padding: '50px 20px', 20 | transition: 'all .5s ease', 21 | zIndex: 9999, 22 | marginRight: '-50%', 23 | transform: 'translate(-50%, -50%)', 24 | boxShadow: '0 5px 10px rgba(0, 0, 0, .1)', 25 | animation: 'scale .3s ease', 26 | ...overrideStyle 27 | } 28 | }; 29 | 30 | AppModal.setAppElement('#app'); 31 | 32 | return ( 33 | 42 | {children} 43 | 44 | ); 45 | }; 46 | 47 | Modal.defaultProps = { 48 | overrideStyle: {}, 49 | afterOpenModal: () => { } 50 | }; 51 | 52 | Modal.propTypes = { 53 | isOpen: PropType.bool.isRequired, 54 | onRequestClose: PropType.func.isRequired, 55 | afterOpenModal: PropType.func, 56 | // eslint-disable-next-line react/forbid-prop-types 57 | overrideStyle: PropType.object, 58 | children: PropType.oneOfType([ 59 | PropType.arrayOf(PropType.node), 60 | PropType.node 61 | ]).isRequired 62 | }; 63 | 64 | export default Modal; 65 | -------------------------------------------------------------------------------- /src/components/common/Preloader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logoWordmark from '../../../static/logo-wordmark.png'; 3 | 4 | const Preloader = () => ( 5 |
6 | 7 | 8 | 9 | 10 | Salinaka logo wordmark 11 |
12 | ); 13 | 14 | export default Preloader; 15 | -------------------------------------------------------------------------------- /src/components/common/PriceRange/SliderRail.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import PropType from 'prop-types'; 3 | import React from 'react'; 4 | 5 | const railOuterStyle = { 6 | position: 'absolute', 7 | transform: 'translate(0%, -50%)', 8 | width: '100%', 9 | height: 42, 10 | borderRadius: 7, 11 | cursor: 'pointer' 12 | // border: '1px solid grey', 13 | }; 14 | 15 | const railInnerStyle = { 16 | position: 'absolute', 17 | width: '100%', 18 | height: 14, 19 | transform: 'translate(0%, -50%)', 20 | borderRadius: 7, 21 | pointerEvents: 'none', 22 | backgroundColor: '#d0d0d0' 23 | }; 24 | 25 | const SliderRail = ({ getRailProps }) => ( 26 |
27 |
28 |
29 |
30 | ); 31 | 32 | SliderRail.propTypes = { 33 | getRailProps: PropType.func.isRequired 34 | }; 35 | 36 | export default SliderRail; 37 | -------------------------------------------------------------------------------- /src/components/common/PriceRange/Tick.jsx: -------------------------------------------------------------------------------- 1 | import PropType from 'prop-types'; 2 | import React from 'react'; 3 | 4 | const Tick = ({ tick, count, format }) => ( 5 |
6 |
16 |
27 | {format(tick.value)} 28 |
29 |
30 | ); 31 | 32 | Tick.propTypes = { 33 | tick: PropType.shape({ 34 | id: PropType.string.isRequired, 35 | value: PropType.number.isRequired, 36 | percent: PropType.number.isRequired 37 | }).isRequired, 38 | count: PropType.number.isRequired, 39 | format: PropType.func 40 | }; 41 | 42 | Tick.defaultProps = { 43 | format: (d) => d 44 | }; 45 | 46 | export default Tick; 47 | -------------------------------------------------------------------------------- /src/components/common/PriceRange/TooltipRail.jsx: -------------------------------------------------------------------------------- 1 | import PropType from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | const railStyle = { 5 | position: 'absolute', 6 | width: '100%', 7 | transform: 'translate(0%, -50%)', 8 | height: 20, 9 | cursor: 'pointer', 10 | zIndex: 300 11 | // border: '1px solid grey', 12 | }; 13 | 14 | const railCenterStyle = { 15 | position: 'absolute', 16 | width: '100%', 17 | transform: 'translate(0%, -50%)', 18 | height: 14, 19 | borderRadius: 7, 20 | cursor: 'pointer', 21 | pointerEvents: 'none', 22 | backgroundColor: '#d0d0d0' 23 | }; 24 | 25 | class TooltipRail extends Component { 26 | constructor(props) { 27 | super(props); 28 | 29 | this.state = { 30 | value: null, 31 | percent: null 32 | }; 33 | } 34 | 35 | onMouseEnter() { 36 | document.addEventListener('mousemove', this.onMouseMove); 37 | } 38 | 39 | onMouseLeave() { 40 | this.setState({ value: null, percent: null }); 41 | document.removeEventListener('mousemove', this.onMouseMove); 42 | } 43 | 44 | onMouseMove(e) { 45 | const { activeHandleID, getEventData } = this.props; 46 | 47 | if (activeHandleID) { 48 | this.setState({ value: null, percent: null }); 49 | } else { 50 | this.setState(getEventData(e)); 51 | } 52 | } 53 | 54 | render() { 55 | const { value, percent } = this.state; 56 | const { activeHandleID, getRailProps } = this.props; 57 | 58 | return ( 59 | <> 60 | {!activeHandleID && value ? ( 61 |
69 |
70 | 71 | Value: 72 | {value} 73 | 74 |
75 |
76 | ) : null} 77 |
85 |
86 | 87 | ); 88 | } 89 | } 90 | 91 | TooltipRail.defaultProps = { 92 | getEventData: undefined, 93 | activeHandleID: undefined, 94 | disabled: false 95 | }; 96 | 97 | TooltipRail.propTypes = { 98 | getEventData: PropType.func, 99 | activeHandleID: PropType.string, 100 | getRailProps: PropType.func.isRequired, 101 | disabled: PropType.bool 102 | }; 103 | 104 | export default TooltipRail; 105 | -------------------------------------------------------------------------------- /src/components/common/PriceRange/Track.jsx: -------------------------------------------------------------------------------- 1 | import PropType from 'prop-types'; 2 | import React from 'react'; 3 | 4 | const Track = ({ 5 | source, target, getTrackProps, disabled 6 | }) => ( 7 |
22 | ); 23 | 24 | Track.propTypes = { 25 | source: PropType.shape({ 26 | id: PropType.string.isRequired, 27 | value: PropType.number.isRequired, 28 | percent: PropType.number.isRequired 29 | }).isRequired, 30 | target: PropType.shape({ 31 | id: PropType.string.isRequired, 32 | value: PropType.number.isRequired, 33 | percent: PropType.number.isRequired 34 | }).isRequired, 35 | getTrackProps: PropType.func.isRequired, 36 | disabled: PropType.bool 37 | }; 38 | 39 | Track.defaultProps = { 40 | disabled: false 41 | }; 42 | 43 | 44 | export default Track; 45 | -------------------------------------------------------------------------------- /src/components/common/SocialLogin.jsx: -------------------------------------------------------------------------------- 1 | import { FacebookOutlined, GithubFilled, GoogleOutlined } from '@ant-design/icons'; 2 | import PropType from 'prop-types'; 3 | import React from 'react'; 4 | import { useDispatch } from 'react-redux'; 5 | import { signInWithFacebook, signInWithGithub, signInWithGoogle } from '@/redux/actions/authActions'; 6 | 7 | const SocialLogin = ({ isLoading }) => { 8 | const dispatch = useDispatch(); 9 | 10 | const onSignInWithGoogle = () => { 11 | dispatch(signInWithGoogle()); 12 | }; 13 | 14 | const onSignInWithFacebook = () => { 15 | dispatch(signInWithFacebook()); 16 | }; 17 | 18 | const onSignInWithGithub = () => { 19 | dispatch(signInWithGithub()); 20 | }; 21 | 22 | return ( 23 |
24 | 34 | 43 | 52 |
53 | ); 54 | }; 55 | 56 | SocialLogin.propTypes = { 57 | isLoading: PropType.bool.isRequired 58 | }; 59 | 60 | export default SocialLogin; 61 | -------------------------------------------------------------------------------- /src/components/common/index.js: -------------------------------------------------------------------------------- 1 | export { default as AdminNavigation } from './AdminNavigation'; 2 | export { default as AdminSideBar } from './AdminSidePanel'; 3 | export { default as Badge } from './Badge'; 4 | export { default as Boundary } from './Boundary'; 5 | export { default as ColorChooser } from './ColorChooser'; 6 | export { default as Filters } from './Filters'; 7 | export { default as FiltersToggle } from './FiltersToggle'; 8 | export { default as Footer } from './Footer'; 9 | export { default as ImageLoader } from './ImageLoader'; 10 | export { default as MessageDisplay } from './MessageDisplay'; 11 | export { default as MobileNavigation } from './MobileNavigation'; 12 | export { default as Modal } from './Modal'; 13 | export { default as Navigation } from './Navigation'; 14 | export { default as Preloader } from './Preloader'; 15 | export { default as PriceRange } from './PriceRange'; 16 | export { default as SearchBar } from './SearchBar'; 17 | export { default as SocialLogin } from './SocialLogin'; 18 | 19 | -------------------------------------------------------------------------------- /src/components/formik/CustomColorInput.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/label-has-associated-control */ 2 | /* eslint-disable react/forbid-prop-types */ 3 | import PropType from 'prop-types'; 4 | import React from 'react'; 5 | 6 | const InputColor = (props) => { 7 | const { 8 | name, form, push, remove 9 | } = props; 10 | const [selectedColor, setSelectedColor] = React.useState(''); 11 | 12 | const handleColorChange = (e) => { 13 | const val = e.target.value; 14 | setSelectedColor(val); 15 | }; 16 | 17 | const handleAddSelectedColor = () => { 18 | if (!form.values[name].includes(selectedColor)) { 19 | push(selectedColor); 20 | setSelectedColor(''); 21 | } 22 | }; 23 | 24 | return ( 25 |
26 |
27 |
28 | {form.touched[name] && form.errors[name] ? ( 29 | {form.errors[name]} 30 | ) : ( 31 | 34 | )} 35 | {selectedColor && ( 36 | <> 37 |
38 |

44 | 45 | Add Selected Color 46 |

47 | 48 | )} 49 |
50 | 56 |
57 |
58 | Selected Color(s) 59 |
60 | {form.values[name]?.map((color, index) => ( 61 |
remove(index)} 64 | className="color-item color-item-deletable" 65 | title={`Remove ${color}`} 66 | style={{ backgroundColor: color }} 67 | role="presentation" 68 | /> 69 | ))} 70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | InputColor.propTypes = { 77 | name: PropType.string.isRequired, 78 | form: PropType.shape({ 79 | values: PropType.object, 80 | touched: PropType.object, 81 | errors: PropType.object 82 | }).isRequired, 83 | push: PropType.func.isRequired, 84 | remove: PropType.func.isRequired 85 | }; 86 | 87 | export default InputColor; 88 | -------------------------------------------------------------------------------- /src/components/formik/CustomCreatableSelect.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/forbid-prop-types */ 2 | import { useField } from 'formik'; 3 | import PropType from 'prop-types'; 4 | import React from 'react'; 5 | import CreatableSelect from 'react-select/creatable'; 6 | 7 | const CustomCreatableSelect = (props) => { 8 | const [field, meta, helpers] = useField(props); 9 | const { 10 | options, defaultValue, label, placeholder, isMulti, type, iid 11 | } = props; 12 | const { touched, error } = meta; 13 | const { setValue } = helpers; 14 | 15 | const handleChange = (newValue) => { 16 | if (Array.isArray(newValue)) { 17 | const arr = newValue.map((fieldKey) => fieldKey.value); 18 | setValue(arr); 19 | } else { 20 | setValue(newValue.value); 21 | } 22 | }; 23 | 24 | const handleKeyDown = (e) => { 25 | if (type === 'number') { 26 | const { key } = e.nativeEvent; 27 | if (/\D/.test(key) && key !== 'Backspace') { 28 | e.preventDefault(); 29 | } 30 | } 31 | }; 32 | 33 | return ( 34 |
35 | {touched && error ? ( 36 | {error} 37 | ) : ( 38 | 39 | )} 40 | ({ 51 | ...provided, 52 | zIndex: 10 53 | }), 54 | container: (provided) => ({ 55 | ...provided, marginBottom: '1.2rem' 56 | }), 57 | control: (provided) => ({ 58 | ...provided, 59 | border: touched && error ? '1px solid red' : '1px solid #cacaca' 60 | }) 61 | }} 62 | /> 63 |
64 | ); 65 | }; 66 | 67 | CustomCreatableSelect.defaultProps = { 68 | isMulti: false, 69 | placeholder: '', 70 | iid: '', 71 | options: [], 72 | type: 'string' 73 | }; 74 | 75 | CustomCreatableSelect.propTypes = { 76 | options: PropType.arrayOf(PropType.object), 77 | defaultValue: PropType.oneOfType([ 78 | PropType.object, 79 | PropType.array 80 | ]).isRequired, 81 | label: PropType.string.isRequired, 82 | placeholder: PropType.string, 83 | isMulti: PropType.bool, 84 | type: PropType.string, 85 | iid: PropType.string 86 | }; 87 | 88 | export default CustomCreatableSelect; 89 | -------------------------------------------------------------------------------- /src/components/formik/CustomInput.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | /* eslint-disable react/forbid-prop-types */ 3 | import PropType from 'prop-types'; 4 | import React from 'react'; 5 | 6 | const CustomInput = ({ 7 | field, form: { touched, errors }, label, inputRef, ...props 8 | }) => ( 9 |
10 | {touched[field.name] && errors[field.name] ? ( 11 | {errors[field.name]} 12 | ) : ( 13 | 14 | )} 15 | 23 |
24 | ); 25 | 26 | CustomInput.defaultProps = { 27 | inputRef: undefined 28 | }; 29 | 30 | CustomInput.propTypes = { 31 | label: PropType.string.isRequired, 32 | field: PropType.object.isRequired, 33 | form: PropType.object.isRequired, 34 | inputRef: PropType.oneOfType([ 35 | PropType.func, 36 | PropType.shape({ current: PropType.instanceOf(Element) }) 37 | ]) 38 | }; 39 | 40 | export default CustomInput; 41 | -------------------------------------------------------------------------------- /src/components/formik/CustomMobileInput.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/forbid-prop-types */ 2 | import { useField } from 'formik'; 3 | import PropType from 'prop-types'; 4 | import React from 'react'; 5 | import PhoneInput from 'react-phone-input-2'; 6 | 7 | const CustomMobileInput = (props) => { 8 | const [field, meta, helpers] = useField(props); 9 | const { label, placeholder, defaultValue } = props; 10 | const { touched, error } = meta; 11 | const { setValue } = helpers; 12 | 13 | const handleChange = (value, data) => { 14 | const mob = { 15 | dialCode: data.dialCode, 16 | countryCode: data.countryCode, 17 | country: data.name, 18 | value 19 | }; 20 | 21 | setValue(mob); 22 | }; 23 | 24 | return ( 25 |
26 | {touched && error ? ( 27 | {error?.value || error?.dialCode} 28 | ) : ( 29 | 30 | )} 31 | 43 |
44 | ); 45 | }; 46 | 47 | CustomMobileInput.defaultProps = { 48 | label: 'Mobile Number', 49 | placeholder: '09254461351' 50 | }; 51 | 52 | CustomMobileInput.propTypes = { 53 | label: PropType.string, 54 | placeholder: PropType.string, 55 | defaultValue: PropType.object.isRequired 56 | }; 57 | 58 | export default CustomMobileInput; 59 | -------------------------------------------------------------------------------- /src/components/formik/CustomTextarea.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | /* eslint-disable react/forbid-prop-types */ 3 | import PropType from 'prop-types'; 4 | import React from 'react'; 5 | 6 | const CustomTextarea = ({ 7 | field, form: { touched, errors }, label, ...props 8 | }) => ( 9 |
10 | {touched[field.name] && errors[field.name] ? ( 11 | {errors[field.name]} 12 | ) : ( 13 | 14 | )} 15 |