diff --git a/README.md b/README.md index c342b251..d14a78fb 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,13 @@ Our e-commerce web application server, developed by Team Ninjas, facilitates smo [![Netlify Status](https://api.netlify.com/api/v1/badges/a3ed5a75-a862-4f3b-ba21-8369180cf3e6/deploy-status)](https://app.netlify.com/sites/e-commerce-ninja-fn-staging/deploys) [![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/e-commerce-ninjas-fe/badge.svg)](https://coveralls.io/github/atlp-rwanda/e-commerce-ninjas-fe) [![CircleCI](https://dl.circleci.com/status-badge/img/gh/atlp-rwanda/e-commerce-ninjas-fe/tree/develop.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/atlp-rwanda/e-commerce-ninjas-fe/tree/develop) +[![CI](https://github.com/atlp-rwanda/e-commerce-ninjas-fe/actions/workflows/ci.yml/badge.svg)](https://github.com/atlp-rwanda/e-commerce-ninjas-fe/actions/workflows/ci.yml) ## Completed features - Setup empty react - Setup Redux store - Setup StoryBook documentation +- Sign up page ## Get started - Clone repository @@ -47,7 +49,7 @@ npm run storybook ## Folder Structure -- `public`: Contains static files like `index.html` and images. +- `public`: Contains static files and folder like `index.html` and images. - `src`: The main source folder for the React application. - `components`: Reusable UI components. - `pages`: Different pages/screens of the application. diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 00000000..7d773237 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,9 @@ +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 + +[[redirects]] + from = "/api/*" + to = "/index.html" + status = 200 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 12f4eab5..f767386e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@reduxjs/toolkit": "^2.2.5", "axios": "^1.7.2", + "dotenv": "^16.4.5", "formik": "^2.4.6", "html-webpack-plugin": "^5.6.0", "mini-css-extract-plugin": "^2.9.0", @@ -19,7 +20,8 @@ "react-icons": "^5.2.1", "react-redux": "^9.1.2", "react-router-dom": "^6.24.0", - "sass": "^1.77.6", + "react-spinners": "^0.14.1", + "react-toastify": "^10.0.5", "save-dev": "0.0.1-security", "yup": "^1.4.0" }, @@ -69,10 +71,12 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-storybook": "^0.8.0", "eslint-webpack-plugin": "^4.2.0", + "file-loader": "^6.2.0", "fork-ts-checker-webpack-plugin": "^9.0.2", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "react-test-renderer": "^18.3.1", + "sass": "^1.77.6", "sass-loader": "^14.2.1", "storybook": "^8.1.10", "style-loader": "^4.0.0", @@ -8642,6 +8646,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -9376,10 +9381,21 @@ "node": ">=0.6" } }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "engines": { "node": ">=8" }, @@ -9486,6 +9502,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -9755,6 +9772,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -9778,6 +9796,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -10054,6 +10073,15 @@ "node": ">=6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -11307,7 +11335,7 @@ "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -11428,6 +11456,16 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -12908,6 +12946,46 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/file-system-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz", @@ -12966,6 +13044,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -13445,6 +13524,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -14334,7 +14414,8 @@ "node_modules/immutable": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==" + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true }, "node_modules/import-fresh": { "version": "3.3.0", @@ -14592,6 +14673,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -14697,6 +14779,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -14751,6 +14834,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -14850,6 +14934,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "engines": { "node": ">=0.12.0" } @@ -17993,6 +18078,21 @@ "node": ">=6.11.5" } }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -18652,6 +18752,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -19471,6 +19572,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -20330,6 +20432,16 @@ "react": "^16.3.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-spinners": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.14.1.tgz", + "integrity": "sha512-2Izq+qgQ08HTofCVEdcAQCXFEYfqTDdfeDQJeo/HHQiQJD4imOicNLhkfN2eh1NYEWVOX4D9ok2lhuDB0z3Aag==", + "license": "MIT", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -20375,6 +20487,19 @@ "dev": true, "license": "MIT" }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -20495,6 +20620,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -21056,6 +21182,8 @@ "version": "1.77.6", "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "dev": true, + "license": "MIT", "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -21073,6 +21201,7 @@ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.2.1.tgz", "integrity": "sha512-G0VcnMYU18a4N7VoNDegg2OuMjYtxnqzQWARVWCIVSZwJeiL9kg8QMsuIZOplsJgTzZLF6jGxI3AClj8I9nRdQ==", "dev": true, + "license": "MIT", "dependencies": { "neo-async": "^2.6.2" }, @@ -21499,6 +21628,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -22374,6 +22504,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, diff --git a/package.json b/package.json index cec8b1f3..4904fdb1 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@reduxjs/toolkit": "^2.2.5", "axios": "^1.7.2", + "dotenv": "^16.4.5", "formik": "^2.4.6", "html-webpack-plugin": "^5.6.0", "mini-css-extract-plugin": "^2.9.0", @@ -21,7 +22,8 @@ "react-icons": "^5.2.1", "react-redux": "^9.1.2", "react-router-dom": "^6.24.0", - "sass": "^1.77.6", + "react-spinners": "^0.14.1", + "react-toastify": "^10.0.5", "save-dev": "0.0.1-security", "yup": "^1.4.0" }, @@ -71,10 +73,12 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-storybook": "^0.8.0", "eslint-webpack-plugin": "^4.2.0", + "file-loader": "^6.2.0", "fork-ts-checker-webpack-plugin": "^9.0.2", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "react-test-renderer": "^18.3.1", + "sass": "^1.77.6", "sass-loader": "^14.2.1", "storybook": "^8.1.10", "style-loader": "^4.0.0", diff --git a/public/images/new-message.png b/public/images/new-message.png new file mode 100644 index 00000000..c8247b52 Binary files /dev/null and b/public/images/new-message.png differ diff --git a/public/images/not-found.png b/public/images/not-found.png new file mode 100644 index 00000000..c3f8355c Binary files /dev/null and b/public/images/not-found.png differ diff --git a/public/images/sign-up.png b/public/images/sign-up.png new file mode 100644 index 00000000..85648b4e Binary files /dev/null and b/public/images/sign-up.png differ diff --git a/src/App.scss b/src/App.scss new file mode 100644 index 00000000..6d0b5082 --- /dev/null +++ b/src/App.scss @@ -0,0 +1,10 @@ +@import url('https://fonts.googleapis.com/css2?family=Averia+Serif+Libre:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap'); +@import './styles/color.scss'; +@import './styles/signup.scss'; +@import './styles/LandingPage.scss'; +@import './styles/email.scss'; + +* { + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 3041e15a..4a89a117 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ /* eslint-disable */ import React from 'react'; -import "./styles/App.scss" +import "./App.scss" import { BrowserRouter as Router } from 'react-router-dom'; import AppRouter from './router'; diff --git a/src/components/buttons/Button.tsx b/src/components/buttons/Button.tsx index 7a279b71..231cb0db 100644 --- a/src/components/buttons/Button.tsx +++ b/src/components/buttons/Button.tsx @@ -1,7 +1,13 @@ +/* eslint-disable */ import React from 'react'; -const Button = ({ title }: { title: string }) => ( - +interface ButtonProps { + title: string; + type: "button" | "submit" | "reset"; +} + +const Button: React.FC = ({ title, type }) => ( + ); -export default Button; +export default Button; \ No newline at end of file diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index db4803dd..186ade90 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -7,7 +7,7 @@ const Header: React.FC = () => ( diff --git a/src/pages/EmailVerifying.tsx b/src/pages/EmailVerifying.tsx new file mode 100644 index 00000000..36020a90 --- /dev/null +++ b/src/pages/EmailVerifying.tsx @@ -0,0 +1,32 @@ +/* eslint-disable */ +import React, { useEffect } from 'react' +import { Meta } from '../components/Meta' +import { useNavigate } from 'react-router-dom' + +export const EmailVerifying = () => { + const navigate = useNavigate(); + useEffect(() => { + setTimeout(() => { + navigate("/") + },6000) + },[navigate]) + return ( + <> + +
+
+
+
+

Email Verification

+

We've sent a verification email to your registered email address. Please check your inbox to activate your account.

+

If you didn't receive the email, please make sure to check your spam folder. If it's still not there, please try resending the verification email.

+
+
+ +
+
+
+
+ + ) +} diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index c246ff35..b4300586 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -3,6 +3,7 @@ import React, { useEffect } from 'react'; import { useAppDispatch, useAppSelector } from '../store/store'; import { IWelcomeMessage } from '../utils/types/store'; import { loadWelcomeMessage } from '../store/features/welcomeSlice'; +import { Meta } from '../components/Meta'; const LandingPage: React.FC = () => { const dispatch = useAppDispatch(); @@ -14,6 +15,7 @@ const LandingPage: React.FC = () => { return ( <> +

{welcomeMessage.message} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx deleted file mode 100644 index 203b7390..00000000 --- a/src/pages/Login.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -import React from 'react' -import { Meta } from '../components/Meta' -import { useFormik } from 'formik' -import * as Yup from 'yup' -import { BiSolidHide } from "react-icons/bi"; - -const LoginSchema = Yup.object().shape({ - email: Yup.string().email().required('Email is required'), - password: Yup.string().required('Password is required'), -}) - -export const Login = () => { - const formik = useFormik({ - initialValues: { - email: '', - password: '', - }, - validationSchema: LoginSchema, - onSubmit: (values) => { - console.log(values) - }, - }) - - return ( - <> - -
-
-
-
-
-
-

- Create e-commerceNinjas account. -

-

- Simplify your e-commerce account with our - user-friendly website. -

-
-
- login-icon -
-
-
-
-

Create an account

-

Please login to your account

-
-
-
- - {formik.touched.email && formik.errors.email ? (

{formik.errors.email}

) : null} -
-
- - {formik.touched.password && formik.errors.password ? (

{formik.errors.password}

) : null} - -
-
-
-

Forgot password

-
-
-
-
-
-
- - ) -} - diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx index 25a6eff3..623eee9e 100644 --- a/src/pages/NotFound.tsx +++ b/src/pages/NotFound.tsx @@ -1,10 +1,24 @@ /* eslint-disable */ import React from 'react'; - +import { Link } from 'react-router-dom'; +import notFound from '../../public/images/not-found.png'; const NotFound: React.FC = () => ( -
-

404 -Not found

-

The page you are looking for does not exist.

+
+
+
+
+ not-found +
+
+

Not found

+

Please make sure you've entered the correct URL.

+

If you think this is a mistake, please contact support.

+
+
+ Back to Home +
+
+
); diff --git a/src/pages/SignUp.tsx b/src/pages/SignUp.tsx new file mode 100644 index 00000000..711381e5 --- /dev/null +++ b/src/pages/SignUp.tsx @@ -0,0 +1,135 @@ +/* eslint-disable */ +import React, { useEffect, useState } from 'react' +import { Meta } from '../components/Meta' +import { useFormik } from 'formik' +import * as Yup from 'yup' +import { BiSolidHide, BiSolidShow } from "react-icons/bi"; +import { FcGoogle } from "react-icons/fc"; +import { useNavigate } from 'react-router-dom'; +import Button from '../components/buttons/Button'; +import { useAppDispatch, useAppSelector } from '../store/store'; +import { registerUser } from '../store/features/auth/authSlice'; +import { CircleLoader } from 'react-spinners'; +import SignUpIcon from '../../public/images/sign-up.png' +const SignUpSchema = Yup.object().shape({ + email: Yup.string().email('Email must be valid').required('Email is required'), + password: Yup.string().required('Password is required'), +}) + +export const SignUp = () => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const { user, isError, isSuccess, isLoading, message } = useAppSelector((state) => state?.auth); + const formik = useFormik({ + initialValues: { + email: '', + password: '', + }, + validationSchema: SignUpSchema, + onSubmit: (values) => { + dispatch(registerUser(values)); + }, + }) + useEffect(() => { + if (isError) { + alert(message); + } + if (isSuccess) { + navigate('/verify-email'); + formik.resetForm(); + } + }, [user, isError, isSuccess, isLoading, message]) + + const [passwordVisible, setPasswordVisible] = useState(false); + const [isFocused, setIsFocused] = useState(false); + const togglePasswordVisibility = () => { + setPasswordVisible(!passwordVisible); + }; + const handleBlur = (e: object) => { + formik.handleBlur(e); + if (!formik.values.password) { + setIsFocused(false) + } + }; + return ( + <> + +
+
+
+
+
+
+

+ Create e-commerceNinjas + account. +

+

+ Simplify your e-commerce account with our + user-friendly website. +

+
+
+ login-icon +
+
+
+
+

Create an account

+

Please create an account to continue

+
+
+
+ + {formik.touched.email && formik.errors.email ? (

{formik.errors.email}

) : null} +
+
+ setIsFocused(true)} + value={formik.values.password} /> + {formik.touched.password && formik.errors.password ? (

{formik.errors.password}

) : null} + {isFocused && ( + {passwordVisible ? : } + )} +
+
+ {isLoading ? ( +
+ +
+ ) : ( +
+
+ )} +
+

or Sign up with

+
+
+
+ +

Continue with google

+
+
+
+
+
+
+
+ + ) +} + diff --git a/src/pages/VerifyEmail.tsx b/src/pages/VerifyEmail.tsx new file mode 100644 index 00000000..dd850276 --- /dev/null +++ b/src/pages/VerifyEmail.tsx @@ -0,0 +1,55 @@ +/* eslint-disable */ +import React, { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { Meta } from "../components/Meta"; +import { useAppDispatch, useAppSelector } from "../store/store"; +import { verifyEmail } from "../store/features/auth/authSlice"; +import { DotLoader } from "react-spinners"; + +const VerifyEmail: React.FC = () => { + const { token } = useParams<{ token: string }>() as any; + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const { isVerified, isError, message, isLoading } = useAppSelector((state) => state.auth) + useEffect(() => { + console.log("token", token); + dispatch(verifyEmail(token)); + }, [token, navigate]); + + useEffect(() => { + if (isVerified) { + navigate('/login') + alert(message); // TODO: Toast to show message when user is verified + } + else if (isError) { + if (message === "Account already verified.") { + navigate('/login') + alert(message); // TODO: Toast to show message when user is already verified + } + if (message === "Account not found.") { + navigate('/signup') + } + navigate('/*') + } + }, [isVerified, isError, navigate] + ); + return ( + <> + +
+
+
+
+ +
+

Verifying your email...

+

Please wait...

+
+
+
+ + + ); +}; + +export default VerifyEmail; diff --git a/src/router.tsx b/src/router.tsx index e0a349c7..9a74dd90 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -2,9 +2,13 @@ import React from 'react'; import { Route, Routes } from 'react-router-dom'; import LandingPage from './pages/LandingPage'; -import { Login } from './pages/Login'; +import { SignUp } from './pages/SignUp'; import NotFound from './pages/NotFound'; import Layout from './components/layout/Layout'; +import VerifyEmail from './pages/VerifyEmail'; +import { EmailVerifying } from './pages/EmailVerifying'; + + const AppRouter: React.FC = () => { return ( @@ -12,7 +16,9 @@ const AppRouter: React.FC = () => { }> } /> - } /> + } /> + } /> + } /> } /> diff --git a/src/store/features/auth/authService.tsx b/src/store/features/auth/authService.tsx new file mode 100644 index 00000000..2a6fb008 --- /dev/null +++ b/src/store/features/auth/authService.tsx @@ -0,0 +1,21 @@ +/* eslint-disable */ +import {axiosInstance} from "../../../utils/axios/axiosInstance"; +import { IUser, IVerification } from "../../../utils/types/store"; + +const register = async(userData: IUser) =>{ + const response = await axiosInstance.post('/api/auth/register', userData); + return response.data; +} + +const verify = async(token:string) =>{ + const response = await axiosInstance.get(`/api/auth/verify-email/${token}`); + return response.data; +} + +const authService = { + register, + verify, + +} + +export default authService; \ No newline at end of file diff --git a/src/store/features/auth/authSlice.tsx b/src/store/features/auth/authSlice.tsx new file mode 100644 index 00000000..dce6c300 --- /dev/null +++ b/src/store/features/auth/authSlice.tsx @@ -0,0 +1,86 @@ +/* eslint-disable */ +import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; +import authService from "./authService"; +import {IUser,IUserData } from "../../../utils/types/store"; +import {getErrorMessage} from '../../../utils/axios/axiosInstance' +const initialState:AuthService= { + user: undefined, + isError: false, + isLoading: false, + isSuccess: false, + isVerified: false, + message: "" +}; + +export interface AuthService{ + user: IUserData | undefined, + isError: boolean, + isLoading: boolean, + isSuccess: boolean, + isVerified: boolean + message: string +} + + +export const registerUser = createAsyncThunk("auth/register", async (userData: IUser, thunkApi) => { + try { + const response = await authService.register(userData); + return response; + } catch (error) { + return thunkApi.rejectWithValue(getErrorMessage(error)); + } +}); + +export const verifyEmail = createAsyncThunk("auth/verify-email", async(token:string, thunkApi)=>{ + try { + const response = await authService.verify(token); + console.log(response) + return response; + } catch (error) { + return thunkApi.rejectWithValue(getErrorMessage(error)); + } +}) + +const userSlice = createSlice({ + name: 'auth', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(registerUser.pending, (state) => { + state.isLoading = true; + state.isError = false; + state.isSuccess = false; + }) + .addCase(registerUser.fulfilled, (state, action: PayloadAction) => { + state.isLoading = false; + state.isSuccess = true; + state.message = action.payload.message; + state.user = action.payload; + }) + .addCase(registerUser.rejected, (state, action:PayloadAction) => { + state.isLoading = false; + state.isSuccess = false; + state.isError = true; + state.message = action.payload; + }) + .addCase(verifyEmail.pending, (state) => { + state.isLoading = true; + state.isError = false; + state.isSuccess = false; + }) + .addCase(verifyEmail.fulfilled, (state, action: PayloadAction) => { + state.isLoading = false; + state.isVerified = true; + state.message = action.payload.message; + }) + .addCase(verifyEmail.rejected, (state, action: PayloadAction) => { + state.isLoading = false; + state.isError = true; + state.message = action.payload; + console.log(action.payload); + }) + }, +}); + +export default userSlice.reducer; diff --git a/src/store/features/welcomeService.tsx b/src/store/features/welcomeService.tsx index 1f2b8278..d6ff9f58 100644 --- a/src/store/features/welcomeService.tsx +++ b/src/store/features/welcomeService.tsx @@ -1,5 +1,5 @@ /* eslint-disable */ -import axiosInstance from '../../utils/axios/axiosInstance'; +import {axiosInstance} from '../../utils/axios/axiosInstance'; import { IWelcomeMessage } from '../../utils/types/store'; const welcome = async () => { diff --git a/src/store/store.ts b/src/store/store.ts index 9e8881ff..96f2b943 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,10 +1,12 @@ import { configureStore } from '@reduxjs/toolkit'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import welcomeReducer from './features/welcomeSlice'; +import authReducer from './features/auth/authSlice'; export const store = configureStore({ reducer: { initialMessage: welcomeReducer, + auth: authReducer, }, }); diff --git a/src/stories/Button.stories.ts b/src/stories/Button.stories.ts index 301a2d02..a7f50452 100644 --- a/src/stories/Button.stories.ts +++ b/src/stories/Button.stories.ts @@ -1,18 +1,32 @@ import type { Meta, StoryObj } from '@storybook/react'; import Button from '../components/buttons/Button'; -const meta = { component: Button } satisfies Meta; +const meta = { + title: 'Example/Button', + component: Button, + argTypes: { + type: { + control: { + type: 'select', + options: ['button', 'submit', 'reset'], + }, + }, + }, +} satisfies Meta; + export default meta; type Story = StoryObj; -export const LongTitle = { +export const LongTitle: Story = { args: { title: 'This is how button will look like with long title', + type: 'button', }, -} satisfies Story; +}; -export const ShortTitle = { +export const ShortTitle: Story = { args: { title: 'Short Btn', + type: 'button', }, -} satisfies Story; +}; diff --git a/src/styles/App.scss b/src/styles/App.scss deleted file mode 100644 index 8593368f..00000000 --- a/src/styles/App.scss +++ /dev/null @@ -1,90 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Averia+Serif+Libre:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap'); -$primary-color: #FF6D18; -$container-color:#D9D9D933; -$login-bg-color: linear-gradient(136deg, #FF6D18 54%, rgba(216, 201, 193, 0.8) 100%); -*{ - margin: 0; - padding: 0; -} -.landingPage { - background: $primary-color; - color: white; - padding: 20px; - - h1 { - font-size: 2em; - margin-bottom: 10px; - } -} -.home-wrapper{ - background-color: $container-color; - padding: 3rem 0; - display: flex; - justify-content: center; - .container{ - max-width: 1280px; - display: flex; - justify-content: center; - align-items: center; - } - - .login-form{ - width: 1126px; - } - - form{ - display: flex; - gap:5rem; - .left-side-login{ - width: 556px; - height: 600px; - background: $login-bg-color; - border-radius: 40px; - padding: 40px; - .left-side-text{ - display: flex; - flex-direction: column; - gap: 2rem; - h1{ - font-family: 'Averia Serif Libre'; - font-weight: 700; - font-size: 40px; - font-style: normal; - margin-top: 3rem; - margin-bottom: 10px; - color: white; - width: 485; - } - p{ - color: #FAF8F8; - font-family: 'Averia Serif Libre'; - font-weight: 700; - font-size: 13px; - width: 270px; - } - - } - .img img{ - margin-top: 1.5rem; - width: 480px; - height: 350px; - } - } - .right-side-login{ - width: 500px; - height: 530px; - display: flex; - flex-direction: column; - align-items: center; - .right-side-text{ - margin-bottom: 2rem; - p,h1{ - text-align: center; - } - .text-left{ - text-align: left; - } - } - } - } -} \ No newline at end of file diff --git a/src/styles/LandingPage.scss b/src/styles/LandingPage.scss new file mode 100644 index 00000000..e060b584 --- /dev/null +++ b/src/styles/LandingPage.scss @@ -0,0 +1,52 @@ +.not-found{ + display: flex; + align-items: center; + flex-direction: column; + + .not-found-img img{ + width: 40vw; + } + .not-found-text{ + font-family: $text-family; + font-weight: 700; + font-size: 1.2rem; + margin-top: 1rem; + margin-bottom: 1rem; + text-align: center; + line-height: 1.5; + letter-spacing: 0.05em; + color: $text-color; + text-decoration: none; + transition: all 0.3s ease-in-out; + } + .btn-link{ + border: none; + background-color: $primary-color; + padding: 1rem 4rem; + text-decoration: none; + color: $white-color; + font-family: $text-family; + font-weight: 700; + font-size: 1.2rem; + border-radius: 2rem; + cursor: pointer; + } + + .btn-link:hover{ + background-color: $white-color; + color: $primary-color; + border: 1px solid $border-color; + box-shadow: 2px 2px 10px rgba(0,0,0,0.5); + transition: all 0.3s ease-in-out; + } +} +.landingPage { + background: $primary-color; + color: $white-color; + padding: 20px; + + h1 { + font-size: 2em; + margin-bottom: 10px; + } + } diff --git a/src/styles/color.scss b/src/styles/color.scss new file mode 100644 index 00000000..c8afd467 --- /dev/null +++ b/src/styles/color.scss @@ -0,0 +1,7 @@ +$primary-color: #FF6D18; +$container-color: #D9D9D933; +$login-bg-color: linear-gradient(136deg, #FF6D18 16%, rgba(216, 201, 193, 0.8) 100%); +$text-color: #666666; +$text-family: 'Averia Serif Libre'; +$white-color: #FFFFFF; +$border-color: #D9D9D9; \ No newline at end of file diff --git a/src/styles/email.scss b/src/styles/email.scss new file mode 100644 index 00000000..e6c5ea6d --- /dev/null +++ b/src/styles/email.scss @@ -0,0 +1,116 @@ +.email-spot{ + width: 80vw; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + background-color: $white-color; + border-radius: 10px; + padding: 2rem; + box-shadow: 2px 2px 10px rgba(0,0,0,0.5); + transition: all 0.3s ease-in-out; + + .text-header{ + text-align: center; + font-family: $text-family; + h1{ + color: $primary-color; + margin-bottom: 1rem; + font-weight: 700; + font-size: 2.7rem; + } + p{ + color: $text-color; + } + } + + .img-message img{ + margin-top: 2rem; + width: 50vw; + height: 70vh; + } +} + +.resend-email{ + width: 80vw; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + background-color: $white-color; + border-radius: 10px; + padding: 2rem; + box-shadow: 2px 2px 10px rgba(0,0,0,0.5); + transition: all 0.3s ease-in-out; + + form{ + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + margin-top: 1rem; + width: 100%; + input{ + width: 50%; + padding: 1rem; + border: 1px solid $border-color; + border-radius: 0.5rem; + outline: none; + font-family: $text-family; + } + button{ + width: 20%; + padding: 1rem; + border: 1px solid $border-color; + border-radius: 0.5rem; + outline: none; + font-family: $text-family; + background-color: $white-color; + color: $primary-color; + font-weight: 700; + font-size: 1.2rem; + border-radius: 2rem; + cursor: pointer; + transition: all 0.3s ease-in-out; + } + button:hover{ + background-color: $primary-color; + color: $white-color; + border: none; + box-shadow: 2px 2px 10px rgba(0,0,0,0.2); + transition: all 0.3s ease-in-out; + } + } +} + +.verify-email-page{ + width: 80vw; + height: 50vh; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + background-color: $white-color; + border-radius: 10px; + padding: 2rem; + box-shadow: 2px 2px 10px rgba(0,0,0,0.5); + transition: all 0.3s ease-in-out; + + .spinner-wrapper { + margin-bottom: 1.5rem; + } + h1,p{ + text-align: center; + font-family: $text-family; + } + h1{ + font-weight: bold; + font-size: 2.7rem; + } + p{ + color: $text-color; + font-style: normal; + font-weight: 400; + } +} + diff --git a/src/styles/signup.scss b/src/styles/signup.scss new file mode 100644 index 00000000..1fc9f058 --- /dev/null +++ b/src/styles/signup.scss @@ -0,0 +1,233 @@ +.wrapper { + background-color: $container-color; + padding: 3rem 0; + display: flex; + justify-content: center; + + .container { + max-width: 1280px; + display: flex; + justify-content: center; + align-items: center; + } + + .login-form { + width: 80vw; + display: flex; + justify-content: center; + align-items: center; + padding: 1.5rem; + } + + form { + display: flex; + gap: 1.5rem; + + .left-side-login { + width: 400px; + height: 500px; + background: $login-bg-color; + border-radius: 40px; + padding: 20px; + + .left-side-text { + display: flex; + flex-direction: column; + gap: 1rem; + justify-content: center; + + h1 { + font-family: $text-family; + font-weight: 700; + font-size: 30px; + font-style: normal; + margin-top: 1.5rem; + margin-bottom: 10px; + color: $white-color; + width: 390px; + } + + p { + color: #FAF8F8; + font-family: $text-family; + font-weight: 700; + font-size: 13px; + width: 270px; + } + + } + + .img{ + margin-bottom: 1.5rem; + img { + width: 320px; + height: 320px; + }} + } + + .right-side-login { + width: 500px; + height: 500px; + display: flex; + flex-direction: column; + align-items: center; + position: relative; + top: 2.5rem; + + .right-side-text { + margin-bottom: 1rem; + + p, + h1 { + text-align: center; + font-family: $text-family; + font-weight: bold; + font-size: 32px; + } + + p { + font-size: 13px; + color: $text-color; + } + } + + .form-box { + display: flex; + flex-direction: column; + gap: 2rem; + + .input-box { + input { + width: 400px; + height: 3.5rem; + border: none; + border: 1px solid #D9D9D9; + border-radius: 20px; + padding-left: 20px; + font-family: $text-family; + font-size: large; + color: $text-color; + outline: none; + } + .error{ + color: red; + padding-left:1rem ; + font-family: $text-family; + } + } + .input-box input:focus{ + border: 1px solid $primary-color; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); + } + .input-box:nth-child(2) { + position: relative; + + .hide { + position: absolute; + top: 1.1rem; + right: 1.5rem; + opacity: 1; + font-size: 20px; + color: $text-color; + cursor: pointer;; + transition: all 0.3s ease-in-out; + } + } + } + + .btn Button,.btn-loading { + background-color: $primary-color; + color: $white-color; + font-family: $text-family; + font-weight: 700; + font-size: 28px; + width: 400px; + height: 3.5rem; + border: none; + border-radius: 20px; + margin-top: 1.5rem; + cursor: pointer; + } + .btn-loading{ + display: flex; + justify-content: center; + align-items: center; + background-color: $white-color; + border: 1px solid $border-color; + } + .btn:hover Button{ + background-color: $white-color; + color: $primary-color; + border: 1px solid $border-color; + background-color: $white-color; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); + transition: all 0.3s ease-in-out; + } + + .line-text { + p { + margin: 1.5rem 0; + text-align: center; + position: relative; + width: 350px; + color: $text-color; + font-size: 18px; + font-weight: bold; + font-family: $text-family; + } + + p::before, + p::after { + content: ""; + position: absolute; + top: 55%; + width: 100px; + height: 1px; + background-color: $text-color; + } + + P::before { + left: 0; + } + + P::after { + right: 0; + } + } + + .google { + display: flex; + justify-content: center; + align-items: center; + gap: 2rem; + border: none; + border-radius: 20px; + border: 1px solid $border-color; + background-color: $white-color; + width: 400px; + height: 3.5rem; + cursor: pointer; + p { + font-family: $text-family; + font-weight: bold; + font-size: 20px; + color: $text-color; + } + + .google-icon { + font-size: 50px; + color: $primary-color; + } + } + .google:hover{ + background-color: $primary-color; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); + transition: all 0.3s ease-in-out; + p{ + color: $white-color; + transition: all 0.3s ease-in-out; + } + } + } + } + } \ No newline at end of file diff --git a/src/test/Button.test.tsx b/src/test/Button.test.tsx index 64928b73..2af823e3 100644 --- a/src/test/Button.test.tsx +++ b/src/test/Button.test.tsx @@ -1,3 +1,4 @@ +// Button.test.tsx /* eslint-disable linebreak-style */ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/extensions */ @@ -9,8 +10,16 @@ import Button from '../components/buttons/Button'; describe('Button Component', () => { it('renders button with correct title', () => { const title = 'Click me'; - render(