diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..8aabf83
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,13 @@
+/**/node_modules/*
+node_modules/
+
+dist/
+build/
+out//**/node_modules/*
+node_modules/
+
+dist/
+build/
+out/
+**/*.cjs
+!**/*.ts
\ No newline at end of file
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..df14a06
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,113 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/eslint-recommended',
+ 'plugin:@typescript-eslint/strict-type-checked',
+ 'plugin:react-hooks/recommended',
+ 'plugin:@typescript-eslint/stylistic-type-checked',
+ 'plugin:react/recommended',
+ 'plugin:react/jsx-runtime',
+ 'plugin:prettier/recommended',
+ 'airbnb',
+ 'plugin:react/jsx-runtime',
+ ],
+ ignorePatterns: ['dist', 'node_modules', 'build', 'public', 'assets'],
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ ecmaVersion: 6,
+ sourceType: 'module',
+ project: ['./tsconfig.json', './tsconfig.node.json'],
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ plugins: [
+ 'react-refresh',
+ '@typescript-eslint',
+ 'unused-imports',
+ 'simple-import-sort',
+ 'prettier',
+ 'unicorn',
+ '@stylistic/eslint-plugin-js',
+ 'react',
+ 'react-hooks',
+ ],
+ rules: {
+ '@typescript-eslint/no-floating-promises': [
+ 'error',
+ {
+ ignoreIIFE: true,
+ },
+ ],
+ eqeqeq: 'error',
+ 'operator-linebreak': ['error', 'after', { overrides: { '?': 'before', ':': 'before' } }],
+ 'no-console': 'warn',
+ 'no-undef': 'off',
+ 'no-unused-vars': 'off',
+ 'no-shadow': 'off',
+ 'implicit-arrow-linebreak': 'off',
+ 'arrow-body-style': 'off',
+ 'object-curly-newline': [
+ 'error',
+ {
+ ObjectExpression: { consistent: true, multiline: true },
+ ObjectPattern: { consistent: true, multiline: true },
+ ImportDeclaration: { consistent: true, multiline: true },
+ ExportDeclaration: { consistent: true, multiline: true },
+ },
+ ],
+ 'import/prefer-default-export': 'off',
+ 'import/no-unresolved': 'error',
+ 'import/no-extraneous-dependencies': [
+ 'error',
+ {
+ devDependencies: true,
+ },
+ ],
+ 'prettier/prettier': 'error',
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
+ '@typescript-eslint/explicit-function-return-type': 'off',
+ '@typescript-eslint/no-explicit-any': 'error',
+ '@typescript-eslint/no-unused-vars': 'warn',
+ '@typescript-eslint/no-use-before-define': 'error',
+ '@typescript-eslint/no-shadow': ['error'],
+
+ 'react/jsx-filename-extension': [1, { extensions: ['.js', '.ts', '.tsx', '.jsx'] }],
+ 'react/jsx-one-expression-per-line': [1, { allow: 'single-child' }],
+ 'react/display-name': 'off',
+ 'react/react-in-jsx-scope': 'off',
+ 'react/require-default-props': [1, { ignoreFunctionalComponents: true }],
+ 'react/function-component-definition': [
+ 2,
+ {
+ namedComponents: 'arrow-function',
+ unnamedComponents: 'arrow-function',
+ },
+ ],
+ 'react-refresh/only-export-components': [
+ 'warn',
+ {
+ allowConstantExport: true,
+ },
+ ],
+ 'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': 'warn',
+ 'jsx-a11y/label-has-associated-control': [
+ 2,
+ {
+ labelComponents: ['CustomInputLabel'],
+ labelAttributes: ['label'],
+ controlComponents: ['CustomInput'],
+ depth: 3,
+ },
+ ],
+ },
+ settings: {
+ 'import/resolver': {
+ typescript: {},
+ },
+ },
+ noInlineConfig: true,
+};
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..e8e9944
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,8 @@
+1. Acceptance criteria:
+
+-
+-
+
+2. Screenshot (not technical tasks):
+
+3. Comments
diff --git a/.gitignore b/.gitignore
index 95045d7..f7d3a28 100644
--- a/.gitignore
+++ b/.gitignore
@@ -106,3 +106,10 @@ dist
# TernJS port file
.tern-port
.DS_Store
+
+# do not track package-lock.json file
+package-lock.json
+
+# Vite
+
+.vite
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000..2d0a86a
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,3 @@
+npm run format
+npm run ci:format
+
diff --git a/.husky/pre-push b/.husky/pre-push
new file mode 100644
index 0000000..a84cc5a
--- /dev/null
+++ b/.husky/pre-push
@@ -0,0 +1,2 @@
+npm run lint
+npm test
diff --git a/.lintstagedrs b/.lintstagedrs
new file mode 100644
index 0000000..45f8716
--- /dev/null
+++ b/.lintstagedrs
@@ -0,0 +1,4 @@
+{
+ "*.{json,tsx,ts,html}": ["prettier --write --ignore-unknown"],
+ "*.{tsx,ts}": ["eslint --quiet --fix"]
+}
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..ba327f8
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,5 @@
+# Add files here to ignore them from prettier formatting
+/dist
+node_modules
+**/*.mp3
+**/*.jpg
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..ebe7974
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,5 @@
+trailingComma: 'all'
+tabWidth: 2
+semi: true
+singleQuote: true
+printWidth: 100
diff --git a/README.md b/README.md
index d416b84..0e4635d 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,178 @@
-# eCommerce-Application
\ No newline at end of file
+# eCommerce-Application
+
+It's a comprehensive online shopping portal that provides an interactive and seamless experience to users. From product discovery to checkout, the application ensures a smooth journey for the user, enhancing their engagement and boosting their purchasing confidence
+
+## Goals
+
+1. Sell products via site
+
+2. RSS students demonstrate acquired knowledge and ability to work in a team.
+
+## Pages
+
+1. Login and Registration pages 🖥️
+2. Main page 🏠
+3. Catalog Product page 📋
+4. Detailed Product page 🔎
+5. User Profile page 👤
+6. Basket page 🛒
+7. About Us page 🙋♂️🙋♀️
+
+## Technology Stack
+
+0. TypeScript
+1. React
+2. Vite
+3. Loadash
+4. AntDesign
+5. SCSS
+6. HTML
+7. CSS
+8. CommerceTools - a leading provider of commerce solutions for B2C and B2B enterprises. CommerceTools offers a cloud-native, microservices-based commerce platform that enables brands to create unique and engaging digital commerce experiences.
+9. Linters: ESLint, Prettier, airbnb rules
+10. Husky
+11. git
+12. Jest
+
+## Organization
+
+0. Agile / Scrum
+1. Jira (Board, Dashboard, Releases, Sprint, Poker Planning, Automation)
+2. Confluence (Knowledge Base, MoMs, Agreements, Roles and Responsibilities etc..)
+3. GitHub (Pull Request, workflow, Review)
+
+## Design
+
+This is a [link](https://www.figma.com/design/vjzFNPME7k3at8mZA0J1AR/Cozy-House?node-id=0%3A1&t=pkImd2amNwJgTpT2-1) to our project design.
+
+## Setup project locally
+
+1. Install Node.js version >= 20.0.0
+2. Clone this [repository](https://github.com/comtvset/eCommerce-Application)
+3. Go to the root of project
+4. To install all dependencies run code
+
+```
+npm i
+```
+
+5. Run project in browser locally
+
+```
+npm run dev
+```
+
+# Scripts:
+
+The project has a few scripts for working with different tools. The common rules how to use them.
+
+1. Open package.json
+2. Find section '_scripts_'
+3. Select the tool you want to apply
+4. Open terminal and run command like: npm run ...
+
+Example:
+
+```
+// run Eslint
+npm run lint
+```
+
+## List of scripts
+
+### dev
+
+to run the whole project in browser using development mode
+
+Example:
+
+```
+npm run dev
+```
+
+### build
+
+to create deployment build for production
+
+Example:
+
+```
+npm run build
+```
+
+### preview
+
+previewing the build locally
+
+Example:
+
+```
+npm run preview
+```
+
+### prepare
+
+automatically added by _husky init_ command to prevent fail
+
+Example:
+
+```
+npm run prepare
+```
+
+### lint
+
+start check of code quality with additional params. At the end you can find list of errors/warning in the terminal:
+
+--cache - check only changed files
+
+--ext - check only file with specified extensions
+
+--report-unused-disable-directives
+This option causes ESLint to report directive comments like // eslint-disable-line when no errors would have been reported on that line anyway.
+
+--max-warnings - Number of warnings to trigger nonzero exit code
+
+Example:
+
+```
+npm run lint
+```
+
+### format
+
+start check of code quality with additional params. At the end you can find list of errors/warning in the terminal:
+
+--cache - check only changed files
+
+--write - format a certain file
+
+Example:
+
+```
+npm run format
+```
+
+### ci:format
+
+start check of code quality with additional params. At the end you can find list of errors/warning in the terminal:
+
+--cache - check only changed files
+
+--write - to format a file in-place
+
+Example:
+
+```
+npm run ci:format
+```
+
+### test
+
+to run the test
+
+Example:
+
+```
+npm run test
+```
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6995b16
--- /dev/null
+++ b/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+ Cozy House
+
+
+
+
diff --git a/netlify.toml b/netlify.toml
new file mode 100644
index 0000000..73f8ded
--- /dev/null
+++ b/netlify.toml
@@ -0,0 +1,5 @@
+[[redirects]]
+from = "/*"
+to = "/index.html"
+status = 200
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..7ed5d17
--- /dev/null
+++ b/package.json
@@ -0,0 +1,84 @@
+{
+ "name": "ecommerce",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "keywords": [
+ "RSS",
+ "ecommerce",
+ "Final task"
+ ],
+ "description": "RSS Final [task](https://github.com/rolling-scopes-school/tasks/tree/master/tasks/eCommerce-Application)",
+ "main": "./src/main.tsx",
+ "husky": {
+ "hooks": {
+ "pre-commit": "lint-staged",
+ "pre-push": "npx validate-branch-name"
+ }
+ },
+ "lint-staged": {
+ "*.{tsx,ts}": "npm run lint:fix"
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint --cache . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "format": "npx prettier . --cache --write src/**/*.*",
+ "ci:format": "prettier . --check",
+ "prepare": "husky || true",
+ "test": "vitest run --coverage"
+ },
+ "license": "ISC",
+ "dependencies": {
+ "@commercetools/platform-sdk": "^7.8.0",
+ "@commercetools/sdk-client-v2": "^2.5.0",
+ "@commercetools/sdk-middleware-auth": "^7.0.1",
+ "@testing-library/jest-dom": "^6.4.5",
+ "@testing-library/react": "^15.0.7",
+ "@types/prop-types": "^15.7.12",
+ "@types/uuid": "^9.0.8",
+ "dotenv": "^16.4.5",
+ "isomorphic-fetch": "^3.0.0",
+ "node-fetch": "^3.3.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-hook-form": "^7.51.4",
+ "react-router-dom": "^6.23.1",
+ "react-slick": "^0.30.2",
+ "slick-carousel": "^1.8.1",
+ "vite-tsconfig-paths": "^4.3.2"
+ },
+ "devDependencies": {
+ "@stylistic/eslint-plugin-js": "^1.7.2",
+ "@types/node": "^20.12.12",
+ "@types/react": "^18.2.66",
+ "@types/react-dom": "^18.2.22",
+ "@types/react-slick": "^0.23.13",
+ "@typescript-eslint/eslint-plugin": "^7.2.0",
+ "@typescript-eslint/parser": "^7.2.0",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "@vitest/coverage-v8": "^1.6.0",
+ "eslint": "^8.57.0",
+ "eslint-config-airbnb": "^19.0.4",
+ "eslint-config-airbnb-base": "^15.0.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-import-resolver-typescript": "^3.6.1",
+ "eslint-plugin-import": "^2.29.1",
+ "eslint-plugin-prettier": "^5.1.3",
+ "eslint-plugin-react": "^7.34.1",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.6",
+ "eslint-plugin-simple-import-sort": "^10.0.0",
+ "eslint-plugin-unicorn": "^50.0.1",
+ "eslint-plugin-unused-imports": "^3.0.0",
+ "husky": "^9.0.11",
+ "lint-staged": "^15.2.0",
+ "prettier": "^3.2.4",
+ "pretty-quick": "^4.0.0",
+ "sass": "^1.75.0",
+ "typescript": "^5.2.2",
+ "vite": "^5.2.0",
+ "vitest": "^1.6.0"
+ }
+}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..12ef905
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,30 @@
+import React, { createContext, useState, useMemo } from 'react';
+import { Router } from 'src/components/router/Router.tsx';
+import { CustomerDraft } from '@commercetools/platform-sdk';
+
+interface CurrentUserContextType {
+ currentUser: CustomerDraft | null;
+ setCurrentUser: React.Dispatch>;
+}
+
+export const CurrentUserContext = createContext(null);
+
+export const App = () => {
+ const [currentUser, setCurrentUser] = useState(null);
+
+ const contextValue = useMemo(
+ () => ({
+ currentUser,
+ setCurrentUser,
+ }),
+ [currentUser, setCurrentUser],
+ );
+
+ return (
+
+
+
+ );
+};
+
+export default App;
diff --git a/src/components/address/Address.module.scss b/src/components/address/Address.module.scss
new file mode 100644
index 0000000..84b319e
--- /dev/null
+++ b/src/components/address/Address.module.scss
@@ -0,0 +1,42 @@
+.checkboxes {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 15px;
+ gap: 15px;
+ align-items: flex-start;
+ width: 160px;
+}
+
+.checkbox_billing {
+ margin: 15px;
+ gap: 15px;
+}
+
+.formbody {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ font-size: 1.4rem;
+ text-transform: uppercase;
+
+ > div {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+
+ > div {
+ font-size: 1.4rem;
+ color: red;
+ margin-top: 5px;
+ text-transform: none;
+ }
+ > label {
+ margin-top: 10px;
+ }
+ }
+}
+
+.error_tooltip {
+ white-space: pre-wrap;
+}
diff --git a/src/components/address/Address.tsx b/src/components/address/Address.tsx
new file mode 100644
index 0000000..e2ac5d8
--- /dev/null
+++ b/src/components/address/Address.tsx
@@ -0,0 +1,122 @@
+import React from 'react';
+import style from 'src/components/address/Address.module.scss';
+import Selector from 'src/components/selector/Selector.tsx';
+import { InputWithLabel } from 'src/components/input/InputWithLabel.tsx';
+import { Checkbox } from 'src/components/checkbox/Checkbox.tsx';
+
+interface AddressProps {
+ formData?: {
+ isShippingDefaultAddress?: boolean;
+ isEqualAddress?: boolean;
+ streetName?: string | undefined;
+ city?: string | undefined;
+ country?: string | undefined;
+ postalCode?: string | undefined;
+ };
+ handleChange: (event: React.ChangeEvent) => void;
+ handleBoolean?: (value: boolean) => void;
+ handleSameAddress?: (value: boolean) => void;
+ errors: {
+ streetName: string | undefined;
+ city: string | undefined;
+ country: string | undefined;
+ postalCode: string | undefined;
+ };
+ title: string;
+ showIsTheSameAddress?: boolean;
+ disabledMode?: boolean;
+}
+
+export const AddressForm: React.FC = ({
+ formData,
+ handleChange,
+ handleBoolean,
+ handleSameAddress,
+ errors,
+ title,
+ showIsTheSameAddress = true,
+ disabledMode = false,
+}) => {
+ const noop = () => {
+ // Intentionally do nothing
+ };
+ const handleCheckboxChange = handleSameAddress ?? noop;
+
+ return (
+
+
{title}
+
+
+ {showIsTheSameAddress && (
+
+ )}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/address/BillingAddress.tsx b/src/components/address/BillingAddress.tsx
new file mode 100644
index 0000000..b89fd3a
--- /dev/null
+++ b/src/components/address/BillingAddress.tsx
@@ -0,0 +1,103 @@
+import React from 'react';
+import style from 'src/components/address/Address.module.scss';
+import Selector from 'src/components/selector/Selector.tsx';
+import { InputWithLabel } from 'src/components/input/InputWithLabel.tsx';
+import { Checkbox } from 'src/components/checkbox/Checkbox.tsx';
+
+interface BillingAddressProps {
+ formData: {
+ isBillingDefaultAddress: boolean;
+ billingStreet: string | undefined;
+ billingCity: string | undefined;
+ billingCountry: string | undefined;
+ billingPostalCode: string | undefined;
+ };
+ handleChange: (event: React.ChangeEvent) => void;
+ handleBoolean: (value: boolean) => void;
+ errors: {
+ billingStreet: string | undefined;
+ billingCity: string | undefined;
+ billingCountry: string | undefined;
+ billingPostalCode: string | undefined;
+ };
+ title: string;
+ disabledMode?: boolean;
+}
+
+export const BillingAddressForm: React.FC = ({
+ formData,
+ handleChange,
+ handleBoolean,
+ errors,
+ title,
+ disabledMode = false,
+}) => {
+ return (
+
+
{title}
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/card/Card.module.scss b/src/components/card/Card.module.scss
new file mode 100644
index 0000000..9ff7a35
--- /dev/null
+++ b/src/components/card/Card.module.scss
@@ -0,0 +1,177 @@
+@import 'src/styles/variables';
+@import 'src/styles/mixins';
+
+.container,
+.main_image {
+ margin: 0 auto;
+}
+
+.container {
+ width: 90%;
+ height: 100vh;
+}
+
+.product {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ padding: 10% 0 10% 0;
+ @include media-1000 {
+ flex-direction: column;
+ }
+}
+
+.container_images,
+.main_image,
+.info {
+ width: 50%;
+}
+
+.image {
+ display: block;
+ width: 100%;
+ height: 40vh;
+ object-fit: contain;
+ transition: transform 0.5s ease;
+ &:hover {
+ cursor: pointer;
+ transform: scale(1.1);
+ }
+}
+
+.images_container {
+ display: flex;
+ width: 60%;
+ margin-top: 20px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.images {
+ display: block;
+ width: 10%;
+ background-color: #ffffff;
+ height: 25vh;
+ object-fit: cover;
+}
+
+.name_thing,
+.description {
+ color: $secondaryTextColor;
+}
+
+.name_thing {
+ margin-top: 0px;
+ font-size: 2.4rem;
+ @include media-1000 {
+ text-align: center;
+ }
+}
+
+.description {
+ font-size: 1.6rem;
+ line-height: 2.5rem;
+}
+
+.container_images {
+ @include media-1200 {
+ width: 65%;
+ }
+ @include media-1000 {
+ width: 80%;
+ margin: 0 auto;
+ }
+ @include media-600 {
+ width: 90%;
+ }
+
+ @include media-400 {
+ width: 100%;
+ }
+}
+
+.info {
+ @include media-1200 {
+ width: 45%;
+ }
+ @include media-1000 {
+ width: 100%;
+ margin-top: 30px;
+ }
+}
+
+.price {
+ font-size: 2rem;
+ color: $secondaryTextColor;
+ margin-bottom: 0px;
+}
+.prices {
+ display: flex;
+ gap: 10%;
+}
+
+.price_start,
+.price_finish {
+ font-size: 2rem;
+ font-weight: 700;
+ margin-top: 10px;
+}
+.price_start {
+ color: $secondaryTextColor;
+}
+.price_sale {
+ text-decoration: line-through;
+}
+.price_finish {
+ color: $priceSale;
+}
+
+.background {
+ position: fixed;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ background-color: rgba(64, 63, 61, 0.9);
+}
+
+.modal_image {
+ display: block;
+ width: 80%;
+ height: 90vh;
+ margin: 0 auto;
+ object-fit: contain;
+ @include media-800 {
+ width: 90%;
+ }
+ @include media-600 {
+ width: 100%;
+ }
+}
+
+.images_container2 {
+ margin-left: auto;
+ margin-right: auto;
+ width: 50%;
+ height: 60%;
+ @include media-1200 {
+ width: 60%;
+ }
+ @include media-800 {
+ width: 70%;
+ }
+ @include media-600 {
+ width: 100%;
+ }
+}
+
+.images2 {
+ display: block;
+ width: 90%;
+ height: 90vh;
+ object-fit: cover;
+}
+
+.images_container2 > ul > li > button::before {
+ color: #ffffff;
+}
diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx
new file mode 100644
index 0000000..04b175f
--- /dev/null
+++ b/src/components/card/Card.tsx
@@ -0,0 +1,231 @@
+import React, { useEffect, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import style from 'src/components/card/Card.module.scss';
+import { Layout } from 'src/components/layout/Layout.tsx';
+import { apiRoot } from 'src/services/api/ctpClient.ts';
+import { Modal } from 'src/components/modalWindow/modalImage.tsx';
+import { Paragraph } from 'src/components/text/Text.tsx';
+import { getCurrencySymbol } from 'src/utils/CurrencyUtils.ts';
+import Slider from 'react-slick';
+import 'slick-carousel/slick/slick.css';
+import 'slick-carousel/slick/slick-theme.css';
+import { ProductCatalogData } from '@commercetools/platform-sdk';
+
+interface Image {
+ url: string;
+}
+
+interface IProductData {
+ id: string;
+ masterData: ProductCatalogData;
+}
+
+export const CardOne: React.FC = () => {
+ const [product, setProduct] = useState();
+ const [error, setError] = useState(null);
+ const [selectedImage, setSelectedImage] = useState(null);
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const [modal, setModal] = useState(false);
+ const { id } = useParams<{ id: string }>();
+
+ const handleImageChange = (newImage: Image, index: number) => {
+ setSelectedImage(newImage);
+ setSelectedIndex(index);
+ };
+
+ const showModalWindow = () => {
+ setModal(true);
+ };
+
+ const closeModalWindow = () => {
+ setModal(false);
+ };
+
+ const isImages = product?.masterData.staged.masterVariant.images ?? [];
+ const isLength = isImages.length > 1;
+ const isFirstPrice = product?.masterData.current.masterVariant.prices;
+ const isDiscount = isFirstPrice
+ ? ((isFirstPrice[0].discounted?.value.centAmount ?? 0) / 100).toFixed(2)
+ : '';
+ const isNoDiscount = isFirstPrice
+ ? ((isFirstPrice[0]?.value.centAmount ?? 0) / 100).toFixed(2).toString()
+ : '';
+ const isCurrencyCode = isFirstPrice ? isFirstPrice[0].discounted?.value.currencyCode : '';
+ const isCurrencyNoDiscount = isFirstPrice ? isFirstPrice[0].value.currencyCode : '';
+ const currencyCode = getCurrencySymbol(isCurrencyCode) ?? '';
+ const currencyNoDiscount = getCurrencySymbol(isCurrencyNoDiscount) ?? '';
+
+ useEffect(() => {
+ if (typeof id !== 'undefined') {
+ apiRoot
+ .products()
+ .withId({ ID: id })
+ .get()
+ .execute()
+ .then((response) => {
+ setProduct(response.body);
+ setError(null);
+ })
+ .catch(() => {
+ setError('An error occurred while including product data. Please generate later');
+ });
+ }
+ }, [id]);
+
+ useEffect(() => {
+ if (product?.masterData.staged.masterVariant.images?.length) {
+ setSelectedImage(product.masterData.staged.masterVariant.images[0]);
+ }
+ }, [product]);
+
+ useEffect(() => {
+ if (modal) {
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = 'unset';
+ }
+ }, [modal]);
+
+ const settings = {
+ dots: true,
+ infinite: true,
+ speed: 500,
+ slidesToShow: isImages.length,
+ slidesToScroll: 1,
+ };
+ const { dots, infinite, speed, slidesToShow, slidesToScroll } = settings;
+
+ const settings2 = {
+ initialSlide: selectedIndex,
+ dot: true,
+ infinit: true,
+ spee: 500,
+ slidesToSho: 1,
+ slidesToScrol: 1,
+ };
+ const { initialSlide, dot, infinit, spee, slidesToSho, slidesToScrol } = settings2;
+
+ return (
+
+ {error && {error}
}
+ {product && (
+
+
+
+ {selectedImage && (
+
{
+ if (event.key === 'Enter' || event.key === ' ') {
+ showModalWindow();
+ }
+ }}
+ >
+
![{product.masterData.current.name['en-US']}]({selectedImage.url})
+
+ )}
+
+ {isLength && (
+
{
+ handleImageChange(isImages[currentSlide], selectedIndex);
+ }}
+ >
+ {product.masterData.staged.masterVariant.images?.map((image) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ {isFirstPrice?.[0].discounted ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+
+
+ )}
+ {product && selectedImage && modal && (
+
+ {!isLength && (
+
+ )}
+ {isLength && (
+ {
+ handleImageChange(isImages[currentSlide], currentSlide);
+ }}
+ >
+ {product.masterData.staged.masterVariant.images?.map((image) => (
+
+ ))}
+
+ )}
+
+ )}
+ {modal && }
+
+ );
+};
diff --git a/src/components/cards/Cards.module.scss b/src/components/cards/Cards.module.scss
new file mode 100644
index 0000000..ac49ef7
--- /dev/null
+++ b/src/components/cards/Cards.module.scss
@@ -0,0 +1,79 @@
+@import 'src/styles/variables';
+
+.cards_container {
+ display: flex;
+ flex-wrap: wrap;
+ width: 80%;
+ padding: 20px;
+ justify-content: space-between;
+ border-radius: 5px;
+}
+
+.card {
+ display: flex;
+ width: 400px;
+ margin-top: 20px;
+ box-shadow: 0px 0px 10px 0px #a5a5a596;
+ justify-content: space-evenly;
+ height: 500px;
+ background-color: #ffffff;
+ flex-direction: column;
+ align-items: center;
+ border-radius: 10px;
+ cursor: pointer;
+ transition: 0.3s ease;
+ user-select: none;
+}
+
+.card:hover {
+ color: hsla(0, 63%, 3%, 0.564);
+ box-shadow: 0px 0px 10px 4px #a5a5a596;
+}
+
+.card:active {
+ transform: scale(0.98);
+}
+
+.image_container {
+ display: flex;
+ width: 90%;
+ justify-content: center;
+ transition: 0.3s ease;
+}
+
+.image_container:hover {
+ transition: 0.3s ease;
+ transform: scale(1.2);
+}
+
+.card_info {
+ display: flex;
+ flex-direction: column;
+ width: 90%;
+ gap: 10px;
+}
+
+.image {
+ object-fit: contain;
+ width: 200px;
+ height: 200px;
+ border-radius: 10px;
+ box-shadow: 0px 0px 4px 1px #a5a5a596;
+ padding: 5px;
+}
+
+.name_thing {
+ color: $secondaryTextColor;
+ margin-bottom: 0px;
+}
+
+.description {
+ margin-bottom: 0px;
+ font-size: 1.6rem;
+}
+
+@media (max-width: 510px) {
+ .image_container:hover {
+ transform: none;
+ }
+}
diff --git a/src/components/cards/Cards.tsx b/src/components/cards/Cards.tsx
new file mode 100644
index 0000000..6b15b1c
--- /dev/null
+++ b/src/components/cards/Cards.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { ProductProjection } from '@commercetools/platform-sdk';
+import { Paragraph } from 'src/components/text/Text.tsx';
+import style from 'src/components/cards/Cards.module.scss';
+import style1 from 'src/components/card/Card.module.scss';
+import { Link } from 'src/components/link/Link.tsx';
+import { getCurrencySymbol } from 'src/utils/CurrencyUtils.ts';
+
+interface CardProps {
+ products: ProductProjection[];
+}
+
+export const Card: React.FC = ({ products }) => {
+ return (
+
+ {products.map((product) => {
+ const priceObj = product.masterVariant.prices?.[0].value;
+ const priceDiscountObj = product.masterVariant.prices?.[0].discounted?.value;
+
+ const centAmount = priceObj?.centAmount;
+ const centAmountDiscount = priceDiscountObj?.centAmount;
+ const currencyCode = priceObj?.currencyCode;
+ const currencySymbol = getCurrencySymbol(currencyCode) ?? '';
+ const price =
+ centAmount !== undefined ? `${currencySymbol}${(centAmount / 100).toFixed(2)}` : '';
+
+ const priceDiscount =
+ centAmountDiscount !== undefined
+ ? `${currencySymbol}${(centAmountDiscount / 100).toFixed(2)}`
+ : '';
+
+ const priceStartClass = centAmountDiscount
+ ? `${style1.price_start} ${style1.price_sale}`
+ : style1.price_start;
+
+ return (
+
+
+
![{product.name['en-US']}]({product.masterVariant.images?.[0].url})
+
+
+
+
+ {centAmountDiscount !== undefined && (
+
+ )}
+
+
+ );
+ })}
+
+ );
+};
diff --git a/src/components/category/Category.module.scss b/src/components/category/Category.module.scss
new file mode 100644
index 0000000..32e84a6
--- /dev/null
+++ b/src/components/category/Category.module.scss
@@ -0,0 +1,41 @@
+.category_container {
+ display: flex;
+ margin-top: 10px;
+ justify-content: center;
+ flex-direction: column;
+ font-size: 1.6rem;
+}
+
+.category {
+ display: flex;
+ flex-direction: column;
+ cursor: pointer;
+ margin: 5px;
+}
+
+.category_parent {
+ display: flex;
+ flex-direction: column;
+ cursor: pointer;
+}
+
+.subcategory {
+ margin-left: 30px;
+ cursor: pointer;
+ padding: 5px;
+}
+
+.category:hover,
+.subcategory:hover {
+ color: orange;
+}
+
+@media (max-width: 500px) {
+ .category_container {
+ font-size: 1.2rem;
+ }
+
+ .subcategory {
+ margin-left: 15px;
+ }
+}
diff --git a/src/components/category/Category.tsx b/src/components/category/Category.tsx
new file mode 100644
index 0000000..2aa85a7
--- /dev/null
+++ b/src/components/category/Category.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import style from 'src/components/category/Category.module.scss';
+
+interface CategoryComponentProps {
+ onCategoryClick: (category: string) => void;
+ selectedCategory: string;
+}
+
+const categories = [
+ { name: 'Decor', isSubcategory: false },
+ { name: 'Wall Decor', isSubcategory: true },
+ { name: 'X-mas', isSubcategory: true },
+ { name: 'Toys', isSubcategory: false },
+ { name: 'Food', isSubcategory: false },
+ { name: 'Jar', isSubcategory: true },
+ { name: 'All category', isSubcategory: false },
+];
+
+export const CategoryComponent: React.FC = ({
+ onCategoryClick,
+ selectedCategory,
+}) => (
+
+
Category:
+
+ {categories.map((category) => (
+
{
+ onCategoryClick(category.name);
+ }}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') onCategoryClick(category.name);
+ }}
+ >
+ {selectedCategory === category.name && '● '}
+ {category.name}
+
+ ))}
+
+
+);
diff --git a/src/components/checkbox/Checkbox.module.scss b/src/components/checkbox/Checkbox.module.scss
new file mode 100644
index 0000000..1c594cd
--- /dev/null
+++ b/src/components/checkbox/Checkbox.module.scss
@@ -0,0 +1,39 @@
+.custom_checkbox {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ input[type='checkbox'] {
+ appearance: none;
+ width: 20px;
+ height: 20px;
+ outline: none;
+ margin: 0 10px 0 0;
+ }
+ label {
+ height: 20px;
+ }
+}
+
+.custom_checkbox input[type='checkbox']::before {
+ content: '';
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ border: 1px solid var(--primary-text-color);
+ border-radius: 4px;
+ background-color: white;
+}
+
+.custom_checkbox input[type='checkbox']:checked:disabled::before,
+.custom_checkbox input[type='checkbox']:disabled::before {
+ background-color: var(--background-color);
+}
+
+.custom_checkbox input[type='checkbox']:checked::before {
+ background-color: var(--background-color);
+ border-color: var(--primary-text-color);
+ outline: none;
+ content: '\2713';
+ background-color: white;
+}
diff --git a/src/components/checkbox/Checkbox.tsx b/src/components/checkbox/Checkbox.tsx
new file mode 100644
index 0000000..48c839b
--- /dev/null
+++ b/src/components/checkbox/Checkbox.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import style from 'src/components/checkbox/Checkbox.module.scss';
+
+interface CheckboxProps {
+ label: string;
+ checked: boolean;
+ id: string;
+ onChange?: (checked: boolean) => void;
+ disabledMode?: boolean;
+}
+
+export const Checkbox: React.FC = ({
+ id,
+ label,
+ checked,
+ onChange,
+ disabledMode = false,
+}) => {
+ const handleChange = (event: React.ChangeEvent) => {
+ onChange?.(event.target.checked);
+ };
+
+ return (
+
+
+
+
+ );
+};
diff --git a/src/components/country/country.ts b/src/components/country/country.ts
new file mode 100644
index 0000000..e29260c
--- /dev/null
+++ b/src/components/country/country.ts
@@ -0,0 +1,14 @@
+export enum Country {
+ France = 'FR',
+ Germany = 'DE',
+ Italy = 'IT',
+ Netherlands = 'NL',
+ Underfined = '...',
+}
+
+export const countryLookup: Record = {
+ FR: 'France',
+ DE: 'Germany',
+ IT: 'Italy',
+ NL: 'Netherlands',
+};
diff --git a/src/components/filter/Filter.module.scss b/src/components/filter/Filter.module.scss
new file mode 100644
index 0000000..e7b8302
--- /dev/null
+++ b/src/components/filter/Filter.module.scss
@@ -0,0 +1,56 @@
+.filters_container {
+ display: flex;
+ flex-direction: column;
+ margin-top: 40px;
+ border-radius: 5px;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ padding: 5px;
+ font-size: 1.7rem;
+ color: (--primary-text-color);
+}
+
+.filter_container {
+ padding: 1rem;
+ border-bottom: 1px solid #ddd;
+ color: (--primary-text-color);
+}
+
+.filter_group {
+ margin-top: 1rem;
+}
+
+.option_group {
+ display: flex;
+ margin-bottom: 0.5rem;
+ align-items: center;
+ flex-direction: row-reverse;
+ justify-content: space-between;
+}
+
+.color_option label {
+ margin-right: 0.5rem;
+}
+
+.input_container {
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 1.6rem;
+}
+
+input {
+ height: initial !important;
+ outline: initial !important;
+}
+
+@media (max-width: 500px) {
+ .filters_container {
+ font-size: 1.2rem;
+ }
+
+ h3 {
+ font-size: 1.6rem;
+ }
+}
diff --git a/src/components/filter/Filter.tsx b/src/components/filter/Filter.tsx
new file mode 100644
index 0000000..b41aac4
--- /dev/null
+++ b/src/components/filter/Filter.tsx
@@ -0,0 +1,108 @@
+import React, { useEffect, useState } from 'react';
+import { ProductProjection } from '@commercetools/platform-sdk';
+import style from 'src/components/filter/Filter.module.scss';
+import myStyles from 'src/components/form/registration/RegistrationForm.module.scss';
+import { fetchAllProducts } from 'src/services/api/filterRequests.ts';
+import { FilterComponent, FilterOption } from './FilterComponent.tsx';
+
+interface FilterProps {
+ colors: FilterOption[];
+ sizes: FilterOption[];
+ prices: FilterOption[];
+ handleChange(event: React.ChangeEvent): void;
+ onReset(products: ProductProjection[]): void;
+}
+
+export const Filter: React.FC = ({
+ colors = [],
+ sizes = [],
+ prices = [],
+ handleChange,
+ onReset,
+}) => {
+ const [selectedOptions, setSelectedOptions] = useState([]);
+ const [productState, setProductState] = useState([]);
+ const errorsArray = [];
+ const trashArray = [];
+
+ const handleFilterChange = (event: React.ChangeEvent) => {
+ const { value } = event.target;
+
+ if (event.target.type === 'radio') {
+ setSelectedOptions([value]);
+ } else if (event.target.checked) {
+ setSelectedOptions([...selectedOptions, value]);
+ } else {
+ setSelectedOptions(selectedOptions.filter((option) => option !== value));
+ }
+
+ handleChange(event);
+ };
+
+ const isChecked = (value: string): boolean => {
+ return selectedOptions.includes(value);
+ };
+
+ useEffect(() => {
+ const errorArray = [];
+ const getAllproducts = async () => {
+ try {
+ const result = await fetchAllProducts();
+ setProductState(result);
+ } catch (error) {
+ errorArray.push(error);
+ }
+ };
+ (async () => {
+ await getAllproducts();
+ })();
+ }, []);
+
+ const resetFilters = async () => {
+ setSelectedOptions([]);
+ try {
+ const result = await fetchAllProducts();
+ setProductState(result);
+ onReset(result);
+ } catch (error) {
+ errorsArray.push(error);
+ }
+ };
+
+ const handleResetButtonClick = () => {
+ resetFilters().catch((error: unknown) => {
+ errorsArray.push(error);
+ trashArray.push(productState);
+ });
+ };
+
+ return (
+
+
Product Filters
+
+
+
+
+
+ );
+};
diff --git a/src/components/filter/FilterComponent.tsx b/src/components/filter/FilterComponent.tsx
new file mode 100644
index 0000000..e96c761
--- /dev/null
+++ b/src/components/filter/FilterComponent.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import style from './Filter.module.scss';
+
+export interface FilterOption {
+ label: string;
+ value: string;
+}
+
+interface FilterComponentProps {
+ options: FilterOption[];
+ title: string;
+ type: 'checkbox' | 'radio';
+ handleChange?(event: React.ChangeEvent): void;
+ checked?: (value: string) => boolean;
+}
+
+export const FilterComponent: React.FC = ({
+ options,
+ title,
+ type,
+ handleChange,
+ checked,
+}) => {
+ return (
+
+
{title}
+
+ {options.map((option) => (
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/form/form.tsx b/src/components/form/form.tsx
new file mode 100644
index 0000000..3207d8d
--- /dev/null
+++ b/src/components/form/form.tsx
@@ -0,0 +1,152 @@
+import React, { useEffect, useState, useCallback } from 'react';
+import styles from 'src/logic/loginPage/loginPage.module.scss';
+import myStyles from 'src/components/form/registration/RegistrationForm.module.scss';
+import { validateEmail, validatePassword } from 'src/components/validation/Validation.ts';
+
+import { myStatus } from 'src/components/tempFolderForDevelop/statusHandler.ts';
+import { IResponse, myRedirect } from 'src/components/tempFolderForDevelop/responseHandler.ts';
+import { ModalWindow } from 'src/components/modalWindow/modalWindow.tsx';
+import { useNavigate } from 'react-router-dom';
+import { Paragraph } from 'src/components/text/Text.tsx';
+import { Link } from 'src/components/link/Link.tsx';
+import { loginRequest } from 'src/services/api/loginRequest.ts';
+import { saveCredentials } from 'src/services/userData/saveEmailPassword.ts';
+
+export const Form = () => {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [errorEmail, setErrorEmail] = useState('');
+ const [errorPassword, setErrorPassword] = useState('');
+ const [showModal, setShowModal] = useState(false);
+ const [modalData, setModalData] = useState(null);
+ const [showPassword, setShowPassword] = useState(false);
+
+ const navigation = useNavigate();
+
+ const handleValidation = useCallback(() => {
+ if (email) {
+ const emailError = validateEmail(email);
+ setErrorEmail(emailError);
+ } else {
+ setErrorEmail('');
+ }
+
+ if (password) {
+ const passwordError = validatePassword(password);
+ setErrorPassword(passwordError);
+ } else {
+ setErrorPassword('');
+ }
+ }, [email, password]);
+
+ const handleClick = async (event: React.MouseEvent) => {
+ event.preventDefault();
+
+ const emailError = validateEmail(email);
+ const passwordError = validatePassword(password);
+
+ if (!email) {
+ setErrorEmail('⚠ This field is required!');
+ }
+ if (!password) {
+ setErrorPassword('⚠ This field is required!');
+ }
+
+ if (emailError.length === 0 && passwordError.length === 0) {
+ try {
+ saveCredentials(email, password);
+ await loginRequest(email, password);
+
+ const responseServer = myStatus(true, 'Congratulations, successful login!');
+ myRedirect(responseServer);
+ setTimeout(() => {
+ setEmail('');
+ setPassword('');
+ navigation('/');
+ }, 1000);
+ setModalData(responseServer);
+ setShowModal(true);
+ } catch (error) {
+ const responseServer = myStatus(false, 'Incorrect email or password!');
+ myRedirect(responseServer);
+ setModalData(responseServer);
+ setShowModal(true);
+ }
+ }
+ };
+
+ useEffect(() => {
+ handleValidation();
+ }, [email, password, handleValidation]);
+
+ useEffect(() => {
+ if (showModal) {
+ const timer = setTimeout(() => {
+ setShowModal(false);
+ }, 1000);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }
+ return () => {
+ ('');
+ };
+ }, [showModal]);
+
+ return (
+ <>
+
+ {showModal && modalData && }
+ >
+ );
+};
diff --git a/src/components/form/profile/AddressProfileForm.tsx b/src/components/form/profile/AddressProfileForm.tsx
new file mode 100644
index 0000000..17c6f84
--- /dev/null
+++ b/src/components/form/profile/AddressProfileForm.tsx
@@ -0,0 +1,405 @@
+import { Address, createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
+import React, { useEffect, useState } from 'react';
+import { getLoginClient } from 'src/services/api/BuildClient.ts';
+import { PROJECT_KEY } from 'src/services/api/BuildClientRegistration.ts';
+import styles from 'src/components/form/profile/UserProfileForm.module.scss';
+import { AddressForm } from 'src/components/address/Address.tsx';
+import { ModalWindow } from 'src/components/modalWindow/modalWindow.tsx';
+import { validateField } from 'src/components/validation/Validation.ts';
+import { validatePostalCode } from 'src/components/validation/PostalCodeValidation.ts';
+import { Country, countryLookup } from 'src/components/country/country.ts';
+import { ICustomerModel, customerModel } from 'src/model/Customer.ts';
+import useModalEffect from 'src/components/form/profile/UseModalEffect.ts';
+import { ServerError } from 'src/utils/error/RequestErrors.ts';
+
+interface AddressProfileProps {
+ userProfileFormData: ICustomerModel;
+}
+
+export const AddressProfileForm: React.FC = ({ userProfileFormData }) => {
+ const apiRoot = createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({
+ projectKey: PROJECT_KEY,
+ });
+
+ const [countryNewAddress] = useState(Country.Underfined);
+ const [countryBilling] = useState(Country.Underfined);
+ const [id] = useState(localStorage.getItem('fullID') ?? '');
+ const [formData, setFormData] = useState({
+ ...customerModel,
+ ...userProfileFormData,
+ });
+ const [, setIsFormValid] = useState(false);
+
+ const popupMessage = { status: '', message: '' };
+ const [modalData, setModalData] = useState(popupMessage);
+ useModalEffect(modalData, setModalData);
+
+ const [errors, setErrors] = useState(customerModel);
+ const [addresses, setAddresses] = useState([]);
+ const [isEditAddress, setIsEditAddress] = useState(false);
+ const [editAddressID, setEditAddressID] = useState('');
+
+ interface IEmptyAddress {
+ streetName: string;
+ city: string;
+ postalCode: string;
+ country: Country;
+ }
+ const emptyAddress: IEmptyAddress = {
+ streetName: '',
+ city: '',
+ postalCode: '',
+ country: Country.Underfined,
+ };
+ const [newAddress, setNewAddress] = useState(emptyAddress);
+
+ const proceedExceptions = (error: unknown, message: string) => {
+ if (error instanceof ServerError) {
+ setModalData({ status: 'Error', message: error.message });
+ } else if (error instanceof Error) {
+ setModalData({ status: 'Error', message: error.message });
+ } else {
+ setModalData({ status: 'Error', message });
+ }
+ };
+
+ useEffect(() => {
+ const apiRoot2 = createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({
+ projectKey: PROJECT_KEY,
+ });
+ const fetchAddresses = async (): Promise => {
+ try {
+ const response = await apiRoot2.customers().withId({ ID: id }).get().execute();
+ setAddresses(response.body.addresses);
+ setFormData((prevFormData) => ({
+ ...prevFormData,
+ version: response.body.version,
+ }));
+ } catch (error) {
+ proceedExceptions(error, 'Could not retrieve customer addresses');
+ }
+ };
+ if (id) {
+ fetchAddresses().catch((error: unknown) => {
+ proceedExceptions(error, 'Could not retrieve customer addresses');
+ });
+ }
+ }, [id]);
+
+ const handleDefaultAddress = (checked: boolean, type: 'shipping' | 'billing') => {
+ setFormData((prevFormData) => ({
+ ...prevFormData,
+ [type === 'shipping' ? 'isShippingDefaultAddress' : 'isBillingDefaultAddress']: checked,
+ }));
+ };
+
+ const validateOneField = (name: string, value: string) => {
+ const error = validateField(name, value, countryNewAddress, countryBilling);
+ setErrors((prevErrors) => ({
+ ...prevErrors,
+ [name]: error || '',
+ }));
+ };
+
+ const handleNewAddressChange = (
+ event: React.ChangeEvent,
+ ) => {
+ const { name, value } = event.target;
+
+ setNewAddress((prevNewAddress) => ({
+ ...prevNewAddress,
+ [name]: value,
+ }));
+ validateOneField(name, value);
+ };
+
+ const fetchLatestVersion = async (): Promise => {
+ try {
+ const response = await apiRoot.customers().withId({ ID: id }).get().execute();
+ return response.body.version;
+ } catch (error) {
+ proceedExceptions(error, 'Fetching latest version');
+ return null;
+ }
+ };
+
+ const handleAddAddress = async () => {
+ const latestVersion = await fetchLatestVersion();
+ if (latestVersion !== null) {
+ try {
+ const response = await apiRoot
+ .customers()
+ .withId({ ID: id })
+ .post({
+ body: {
+ version: latestVersion,
+ actions: [
+ {
+ action: 'addAddress',
+ address: {
+ streetName: newAddress.streetName,
+ city: newAddress.city,
+ postalCode: newAddress.postalCode,
+ country: Country[newAddress.country as keyof typeof Country],
+ },
+ },
+ ],
+ },
+ })
+ .execute();
+
+ setAddresses(response.body.addresses);
+ setFormData((prevFormData) => ({
+ ...prevFormData,
+ version: response.body.version,
+ }));
+ setNewAddress(emptyAddress);
+ setModalData({ status: 'Success', message: 'Address added successfully' });
+ } catch (error) {
+ setModalData({ status: 'Error', message: 'Could not add a new address' });
+ proceedExceptions(error, 'Adding new address');
+ }
+ }
+ };
+
+ const handleEditAddress = async () => {
+ const latestVersion = await fetchLatestVersion();
+ if (latestVersion !== null) {
+ try {
+ const response = await apiRoot
+ .customers()
+ .withId({ ID: id })
+ .post({
+ body: {
+ version: latestVersion,
+ actions: [
+ {
+ action: 'changeAddress',
+ addressId: editAddressID,
+ address: {
+ streetName: newAddress.streetName,
+ city: newAddress.city,
+ postalCode: newAddress.postalCode,
+ country: Country[newAddress.country as keyof typeof Country],
+ },
+ },
+ ],
+ },
+ })
+ .execute();
+
+ setAddresses(response.body.addresses);
+ setIsEditAddress(false);
+ setFormData((prevFormData) => ({
+ ...prevFormData,
+ version: response.body.version,
+ }));
+ setNewAddress(emptyAddress);
+ setModalData({ status: 'Success', message: 'Address edited successfully' });
+ } catch (error) {
+ setModalData({ status: 'Error', message: 'Could not save edited address' });
+ proceedExceptions(error, 'Editing address');
+ }
+ }
+ };
+
+ const handleDeleteAddress = async (addressId: string) => {
+ const latestVersion = await fetchLatestVersion();
+ if (latestVersion !== null) {
+ try {
+ const response = await apiRoot
+ .customers()
+ .withId({ ID: id })
+ .post({
+ body: {
+ version: latestVersion,
+ actions: [
+ {
+ action: 'removeAddress',
+ addressId,
+ },
+ ],
+ },
+ })
+ .execute();
+
+ setAddresses(response.body.addresses);
+ setFormData((prevFormData) => ({
+ ...prevFormData,
+ version: response.body.version,
+ }));
+ setModalData({ status: 'Success', message: 'Address deleted successfully' });
+ } catch (error) {
+ setModalData({ status: 'Error', message: 'Error deleting address' });
+ proceedExceptions(error, 'Deleting address');
+ }
+ }
+ };
+
+ useEffect(() => {
+ const allFieldsValid = Object.values(errors).every(
+ (error) => error === '' || typeof error === 'boolean',
+ );
+ setIsFormValid(allFieldsValid);
+ }, [errors]);
+
+ useEffect(() => {
+ const error = validatePostalCode(countryNewAddress, newAddress.postalCode);
+ setErrors((prevErrors) => ({
+ ...prevErrors,
+ postalCode: error,
+ }));
+ }, [countryNewAddress, newAddress.postalCode]);
+
+ useEffect(() => {
+ const error = validatePostalCode(countryBilling, formData.billingPostalCode);
+ setErrors((prevErrors) => ({
+ ...prevErrors,
+ billingPostalCode: error,
+ }));
+ }, [countryBilling, formData.billingPostalCode]);
+
+ return (
+ <>
+
+
+
Add New Address
+
+
+
+ {isEditAddress && (
+
+ )}
+ {isEditAddress && (
+
+ )}
+
+
+ {!isEditAddress && (
+
+ )}
+
+
+
Your Addresses
+ {addresses
+ .slice()
+ .reverse()
+ .map((address) => (
+
+
+
+
+ {!isEditAddress && (
+
+ )}
+
+
+
+
+
+
+
street name:
+
{address.streetName}
+
+
+
city:
+
{address.city}
+
+
+
postal code:
+
{address.postalCode}
+
+
+
country:
+
{countryLookup[address.country]}
+
+
+
+ ))}
+
+
+
+ {modalData.message && }
+ >
+ );
+};
diff --git a/src/components/form/profile/BasicUserDataProfile.tsx b/src/components/form/profile/BasicUserDataProfile.tsx
new file mode 100644
index 0000000..0b5e7f4
--- /dev/null
+++ b/src/components/form/profile/BasicUserDataProfile.tsx
@@ -0,0 +1,194 @@
+import React, { useEffect, useState } from 'react';
+import { createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
+import { Country } from 'src/components/country/country.ts';
+import { ModalWindow } from 'src/components/modalWindow/modalWindow.tsx';
+import { ICustomerModel, customerModel } from 'src/model/Customer.ts';
+import { getLoginClient } from 'src/services/api/BuildClient.ts';
+import { PROJECT_KEY } from 'src/services/api/BuildClientRegistration.ts';
+import { RegistrationMainFields } from 'src/components/form/registration/RegistrationMainFields.tsx';
+import styles from 'src/components/form/profile/UserProfileForm.module.scss';
+import { validateField } from 'src/components/validation/Validation.ts';
+import { updateCustomerField } from 'src/services/api/updateCustomer.ts';
+import { updateEmail } from 'src/services/userData/saveEmailPassword.ts';
+import { mapCustomerToModel } from 'src/services/DTO/Customer.ts';
+import { ServerError } from 'src/utils/error/RequestErrors.ts';
+import useModalEffect from './UseModalEffect.ts';
+
+interface BasicUserDataProfileProps {
+ userProfileFormData: ICustomerModel;
+}
+
+export const BasicUserDataProfile: React.FC = ({
+ userProfileFormData,
+}) => {
+ const apiRoot2 = createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({
+ projectKey: PROJECT_KEY,
+ });
+ const [api, setAPI] = useState(apiRoot2);
+
+ const [id] = useState(localStorage.getItem('fullID') ?? '');
+ const [isDisabledUserInfo, setEditUserInfo] = useState(true);
+ const [formData, setFormData] = useState(customerModel);
+ const [isFormValid, setIsFormValid] = useState(false);
+ const [isEmail, setEmail] = useState(false);
+
+ const popupMessage = { status: '', message: '' };
+ const [modalData, setModalData] = useState(popupMessage);
+ useModalEffect(modalData, setModalData);
+
+ const [errors, setErrors] = useState(customerModel);
+
+ const proceedExceptions = (error: unknown, message: string) => {
+ if (error instanceof ServerError) {
+ setModalData({ status: 'Error', message: error.message });
+ } else if (error instanceof Error) {
+ setModalData({ status: 'Error', message: error.message });
+ } else {
+ setModalData({ status: 'Error', message });
+ }
+ };
+
+ const fetchLatestVersion = async (): Promise => {
+ try {
+ const response = await api.customers().withId({ ID: id }).get().execute();
+ return response.body.version;
+ } catch (error) {
+ proceedExceptions(error, 'Fetching latest version');
+ return null;
+ }
+ };
+
+ useEffect(() => {
+ setFormData({
+ ...userProfileFormData,
+ });
+ }, [id, userProfileFormData]);
+
+ const validateOneField = (name: string, value: string) => {
+ const error = validateField(name, value, Country.Underfined, Country.Underfined);
+ const errorValidate = error === '' ? '' : error;
+ setErrors({
+ ...errors,
+ [name]: errorValidate,
+ });
+ };
+
+ const handleChange = (event: React.ChangeEvent) => {
+ const { name, value } = event.target;
+ setFormData({
+ ...formData,
+ [name]: value,
+ });
+
+ validateOneField(name, value);
+ };
+
+ useEffect(() => {
+ const allFieldsValid = Object.values(errors).every(
+ (error) => error === '' || typeof error === 'boolean',
+ );
+ setIsFormValid(allFieldsValid);
+ }, [errors, formData.firstName, formData.lastName, formData.dateOfBirth, formData.email]);
+
+ const handleUserInfoTab = async () => {
+ try {
+ const bodyRequest = {
+ firstName: formData.firstName,
+ lastName: formData.lastName,
+ dateOfBirth: formData.dateOfBirth,
+ email: formData.email,
+ };
+ if (isDisabledUserInfo) {
+ setEmail(true);
+ setEditUserInfo(false);
+ } else {
+ setEditUserInfo(true);
+ setEmail(false);
+
+ if (isFormValid) {
+ if (id) {
+ const latestVersion = await fetchLatestVersion();
+
+ updateCustomerField(
+ api,
+ id,
+ latestVersion ?? -1,
+ formData.firstName ?? '',
+ formData.lastName ?? '',
+ formData.dateOfBirth ?? '',
+ formData.email,
+ )
+ .then((response) => {
+ const customerData = mapCustomerToModel(response.body);
+
+ updateEmail(formData.email);
+
+ setAPI(
+ createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({
+ projectKey: PROJECT_KEY,
+ }),
+ );
+
+ setFormData(customerData);
+ setModalData({
+ status: 'Success',
+ message: 'You have successfully updated your details',
+ });
+ })
+ .catch((error: unknown) => {
+ if (error instanceof ServerError) {
+ setModalData({ status: 'Error', message: error.message });
+ } else if (error instanceof Error) {
+ setModalData({ status: 'Error', message: error.message });
+ } else {
+ setModalData({ status: 'Error', message: 'Unexpected error occurred.' });
+ }
+ setFormData({
+ ...formData,
+ firstName: bodyRequest.firstName,
+ lastName: bodyRequest.firstName,
+ dateOfBirth: bodyRequest.dateOfBirth,
+ email: bodyRequest.email,
+ });
+ });
+ } else {
+ setModalData({ status: 'Error', message: 'Please, make relogin again.' });
+ }
+ }
+ }
+ } catch (error) {
+ // TODO
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+ {modalData.message && }
+ >
+ );
+};
diff --git a/src/components/form/profile/ChangePasswordForm.tsx b/src/components/form/profile/ChangePasswordForm.tsx
new file mode 100644
index 0000000..f2e0f7c
--- /dev/null
+++ b/src/components/form/profile/ChangePasswordForm.tsx
@@ -0,0 +1,237 @@
+import React, { useEffect, useState } from 'react';
+import styleAddr from 'src/components/address/Address.module.scss';
+import styles from 'src/components/form/profile/UserProfileForm.module.scss';
+import { InputWithLabel } from 'src/components/input/InputWithLabel.tsx';
+import { IPasswordForm, passwordForm } from 'src/components/form/profile/IPasswordForm.ts';
+import { validatePassword } from 'src/components/validation/Validation.ts';
+import { ModalWindow } from 'src/components/modalWindow/modalWindow.tsx';
+import { createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
+import { getLoginClient } from 'src/services/api/BuildClient.ts';
+import { PROJECT_KEY } from 'src/services/api/BuildClientRegistration.ts';
+import { ServerError } from 'src/utils/error/RequestErrors.ts';
+import { getPassword, setPassword } from 'src/services/userData/saveEmailPassword.ts';
+import { updatePassword } from 'src/services/api/ResetPassword.ts';
+import useModalEffect from 'src/components/form/profile/UseModalEffect.ts';
+
+interface ChangePasswordFormProps {
+ version: number;
+}
+
+export const ChangePasswordForm: React.FC = ({ version }) => {
+ const apiRoot2 = createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({
+ projectKey: PROJECT_KEY,
+ });
+ const [api, setAPI] = useState(apiRoot2);
+
+ const [isDisabledPassword, setEditPassword] = useState(true);
+ const [errors, setErrors] = useState(passwordForm);
+ const [formData, setFormData] = useState(passwordForm);
+ const popupMessage = { status: '', message: '' };
+ const [modalData, setModalData] = useState(popupMessage);
+ useModalEffect(modalData, setModalData);
+
+ const [id] = useState(localStorage.getItem('fullID') ?? '');
+ const [, setCustomerVersion] = useState(version);
+ const [isFormValid, setIsFormValid] = useState(false);
+ const [isOldPasswordCorrect, setIsOldPasswordCorrect] = useState(false);
+ const [isSamePasswords, setIsSamePasswords] = useState(false);
+
+ const proceedExceptions = (error: unknown, message: string) => {
+ if (error instanceof ServerError) {
+ setModalData({ status: 'Error', message: error.message });
+ } else if (error instanceof Error) {
+ setModalData({ status: 'Error', message: error.message });
+ } else {
+ setModalData({ status: 'Error', message });
+ }
+ };
+
+ const fetchLatestVersion = async (): Promise => {
+ let response;
+ let result = -1;
+
+ try {
+ response = await api.customers().withId({ ID: id }).get().execute();
+ } catch (error) {
+ proceedExceptions(error, 'Fetching latest version');
+ }
+ if (response !== undefined) {
+ result = response.body.version;
+ }
+ return result;
+ };
+
+ const validatePasswordsForm = (name: string, value: string) => {
+ const error = validatePassword(value);
+ const errorValidate = error === '' ? '' : error;
+ setErrors({
+ ...errors,
+ [name]: errorValidate,
+ });
+ };
+
+ const handleChange = (event: React.ChangeEvent) => {
+ const { name, value } = event.target;
+ setFormData({
+ ...formData,
+ [name]: value,
+ });
+
+ validatePasswordsForm(name, value);
+ };
+
+ useEffect(() => {
+ const allFieldsValid = Object.values(errors).every(
+ (error) => error === '' || typeof error === 'boolean',
+ );
+ setIsFormValid(allFieldsValid);
+ }, [errors, formData]);
+
+ const handlePasswordChange = async () => {
+ if (id && isSamePasswords && formData.oldPassword) {
+ if (localStorage.getItem('changePassword')) {
+ setModalData({
+ status: 'Invalid',
+ message: 'Please logout and login again after password was changed.',
+ });
+ } else {
+ const latestVersion = await fetchLatestVersion();
+
+ updatePassword(api, formData, latestVersion)
+ .then((response) => {
+ setCustomerVersion(response.body.version);
+ setPassword(formData.newPassword);
+ setFormData(passwordForm);
+ setIsOldPasswordCorrect(false);
+ localStorage.setItem('changePassword', 'true');
+
+ setModalData({ status: 'Success', message: 'Password updated successfully.' });
+ setEditPassword(true);
+ setAPI(
+ createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({
+ projectKey: PROJECT_KEY,
+ }),
+ );
+ })
+ .catch((error: unknown) => {
+ if (error instanceof ServerError) {
+ setModalData({ status: 'Error', message: error.message });
+ } else {
+ setModalData({ status: 'Error', message: 'Unexpected error occurred.' });
+ }
+ });
+ }
+ } else {
+ setModalData({
+ status: 'Invalid',
+ message: 'The New Password and Confirm Password should be the same',
+ });
+ }
+ };
+
+ const handleOldPassword = () => {
+ if (formData.oldPassword === getPassword()) {
+ setIsOldPasswordCorrect(true);
+ setEditPassword(false);
+ setIsSamePasswords(false);
+ setModalData({ status: 'Sucess!', message: 'Password sucessfuly verifyed' });
+ } else {
+ setIsOldPasswordCorrect(false);
+ setEditPassword(true);
+ setModalData({ status: 'Error', message: 'Current Password is wrong. Try again.' });
+ }
+ };
+
+ const isConfirmPassword = () => {
+ if (
+ formData.newPassword === formData.confirmPassword &&
+ isOldPasswordCorrect &&
+ formData.newPassword &&
+ formData.confirmPassword &&
+ isFormValid
+ ) {
+ setIsSamePasswords(true);
+ } else setIsSamePasswords(false);
+ };
+
+ useEffect(() => {
+ if (
+ formData.newPassword === formData.confirmPassword &&
+ isOldPasswordCorrect &&
+ formData.newPassword &&
+ formData.confirmPassword &&
+ isFormValid
+ ) {
+ setIsSamePasswords(true);
+ } else setIsSamePasswords(false);
+ }, [formData.confirmPassword, formData.newPassword, isFormValid, isOldPasswordCorrect]);
+
+ return (
+ <>
+
+
+ {
+ handleChange(e);
+ }}
+ required
+ error={errors.oldPassword}
+ />
+
+
+ {
+ handleChange(e);
+ isConfirmPassword();
+ }}
+ required
+ error={errors.newPassword}
+ disabledMode={isDisabledPassword}
+ />
+
+ {
+ handleChange(e);
+ isConfirmPassword();
+ }}
+ required
+ error={errors.confirmPassword}
+ disabledMode={isDisabledPassword}
+ />
+
+ {modalData.message &&
}
+
+ {isOldPasswordCorrect && (
+
+ )}
+ >
+ );
+};
diff --git a/src/components/form/profile/IPasswordForm.ts b/src/components/form/profile/IPasswordForm.ts
new file mode 100644
index 0000000..82c6891
--- /dev/null
+++ b/src/components/form/profile/IPasswordForm.ts
@@ -0,0 +1,11 @@
+export interface IPasswordForm {
+ oldPassword: string;
+ newPassword: string;
+ confirmPassword: string;
+}
+
+export const passwordForm: IPasswordForm = {
+ oldPassword: '',
+ newPassword: '',
+ confirmPassword: '',
+};
diff --git a/src/components/form/profile/UseModalEffect.ts b/src/components/form/profile/UseModalEffect.ts
new file mode 100644
index 0000000..3bb3637
--- /dev/null
+++ b/src/components/form/profile/UseModalEffect.ts
@@ -0,0 +1,23 @@
+import { useEffect } from 'react';
+
+const useModalEffect = (
+ modalData: { status: string; message: string },
+ setModalData: React.Dispatch>,
+) => {
+ useEffect(() => {
+ if (modalData.status) {
+ const timer = setTimeout(() => {
+ setModalData({ status: '', message: '' });
+ }, 4000);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }
+ return () => {
+ ('');
+ };
+ }, [modalData, setModalData]);
+};
+
+export default useModalEffect;
diff --git a/src/components/form/profile/UserProfileForm.module.scss b/src/components/form/profile/UserProfileForm.module.scss
new file mode 100644
index 0000000..3ef35a8
--- /dev/null
+++ b/src/components/form/profile/UserProfileForm.module.scss
@@ -0,0 +1,155 @@
+@import 'src/styles/mixins.scss';
+
+.tab_container {
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ flex-direction: column;
+ margin: 50px 50px;
+ > div {
+ width: 100%;
+ }
+}
+
+.tabs {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+ border-bottom: 2px solid #ddd;
+ flex-wrap: wrap;
+}
+
+.tab {
+ padding: 10px 20px;
+ cursor: pointer;
+ background-color: #efe6d3;
+ border: 1px solid #ddd;
+ border-bottom: none;
+ border-radius: 5px 5px 0 0;
+ transition:
+ background-color 0.3s,
+ color 0.3s;
+}
+
+.tab:hover {
+ background-color: #b4d23d;
+}
+
+.activeTab {
+ background-color: #94b21b;
+ color: white;
+ :hover {
+ background-color: #b4d23d;
+ }
+}
+
+.content {
+ @include customer-info-block;
+}
+
+.content.hidden {
+ opacity: 0;
+ position: absolute;
+ pointer-events: none;
+}
+
+.content.visible {
+ opacity: 1;
+ position: relative;
+}
+
+.addresses {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-between;
+
+ > div {
+ @include customer-info-block;
+ width: 45%;
+ }
+}
+
+.verifyButton {
+ align-self: self-start;
+ margin: 15px 0;
+ height: 40px;
+ border-radius: 10px;
+ padding: 10px;
+ background-color: var(--register-button);
+}
+
+.updteButton {
+ align-self: self-start;
+ margin: 15px 0;
+ height: 40px;
+ border-radius: 10px;
+ padding: 10px;
+ background-color: var(--hover-text-color);
+}
+
+.addressRow {
+ display: flex;
+ flex-direction: row;
+}
+
+.newaddress_card {
+ display: flex;
+ flex-direction: column;
+}
+
+.address_list {
+}
+
+.address_row {
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+ font-size: 1rem;
+ text-transform: uppercase;
+ > div > span {
+ font-weight: 600;
+ }
+}
+
+.editdelete_container {
+ display: flex;
+ justify-content: space-between;
+ align-items: stretch;
+ margin-bottom: 20px;
+ .address_checkboxes {
+ display: flex;
+ flex-direction: row;
+ gap: 30px;
+ }
+}
+
+.buttons_editaddress {
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+}
+
+.addresses_container {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: nowrap;
+}
+.addresses {
+ flex-direction: column;
+ padding: 10px 0px;
+ margin-right: 20px;
+ > div {
+ width: 100%;
+ }
+}
+
+@media (width < 650px) {
+ .tab_container {
+ margin: 10px 10px;
+ }
+
+ .tabs {
+ border-bottom: none;
+ }
+}
diff --git a/src/components/form/profile/UserProfileForm.tsx b/src/components/form/profile/UserProfileForm.tsx
new file mode 100644
index 0000000..02a1b7c
--- /dev/null
+++ b/src/components/form/profile/UserProfileForm.tsx
@@ -0,0 +1,105 @@
+import React, { useEffect, useState } from 'react';
+import { ICustomerModel, customerModel } from 'src/model/Customer.ts';
+import styles from 'src/components/form/profile/UserProfileForm.module.scss';
+import { createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
+
+import { getLoginClient } from 'src/services/api/BuildClient.ts';
+
+import { ModalWindow } from 'src/components/modalWindow/modalWindow.tsx';
+
+import { ServerError } from 'src/utils/error/RequestErrors.ts';
+import { mapCustomerToModel } from 'src/services/DTO/Customer.ts';
+
+import { ChangePasswordForm } from 'src/components/form/profile/ChangePasswordForm.tsx';
+import { AddressProfileForm } from 'src/components/form/profile/AddressProfileForm.tsx';
+import useModalEffect from 'src/components/form/profile/UseModalEffect.ts';
+import { PROJECT_KEY } from 'src/services/api/BuildClientRegistration.ts';
+import { BasicUserDataProfile } from './BasicUserDataProfile.tsx';
+
+export const UserProfileForm: React.FC = () => {
+ const apiRoot2 = createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({
+ projectKey: PROJECT_KEY,
+ });
+ const [api] = useState(apiRoot2);
+ const [activeTab, setActiveTab] = useState('basicInfo');
+
+ const [id] = useState(localStorage.getItem('fullID') ?? '');
+ const [customerVersion, setCustomerVersion] = useState(-1);
+
+ const [formData, setFormData] = useState(customerModel);
+
+ const popupMessage = { status: '', message: '' };
+ const [modalData, setModalData] = useState(popupMessage);
+ useModalEffect(modalData, setModalData);
+
+ useEffect(() => {
+ if (id) {
+ const initTabByCustomerData = async () => {
+ return api.customers().withId({ ID: id }).get().execute();
+ };
+ initTabByCustomerData()
+ .then((response) => {
+ const customerData = mapCustomerToModel(response.body);
+ setFormData(customerData);
+ setCustomerVersion(response.body.version);
+ })
+ .catch((error: unknown) => {
+ if (error instanceof ServerError) {
+ setModalData({ status: 'Error', message: error.message });
+ } else {
+ setModalData({ status: 'Error', message: 'Unexpected error occurred.' });
+ }
+ });
+ } else {
+ setModalData({ status: 'Error', message: 'Please, make relogin again.' });
+ }
+ }, [id, api, activeTab]);
+
+ const handleSwitchTab = () => {
+ switch (activeTab) {
+ case 'basicInfo':
+ return ;
+ case 'address':
+ return ;
+ case 'password':
+ return ;
+ default:
+ return null;
+ }
+ };
+ return (
+
+
+
+
+
+
+
{handleSwitchTab()}
+ {modalData.message &&
}
+
+ );
+};
diff --git a/src/components/form/registration/RegistrationForm.module.scss b/src/components/form/registration/RegistrationForm.module.scss
new file mode 100644
index 0000000..96bd975
--- /dev/null
+++ b/src/components/form/registration/RegistrationForm.module.scss
@@ -0,0 +1,106 @@
+@import 'src/styles/mixins';
+@import 'src/styles/variables';
+
+.registration {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ height: 100vw;
+ background-color: var(--background-color);
+ font-size: 1.3rem;
+ flex-wrap: nowrap;
+ align-content: center;
+ input {
+ width: 400px;
+ }
+}
+
+.submitButton {
+ background-color: var(--register-button);
+ border-radius: 0.5rem;
+ height: auto;
+ padding: 0.5rem 0;
+ letter-spacing: 0.05rem;
+ border: 1px solid var(--background-color);
+ color: var(--background-color);
+ font-weight: 600;
+ cursor: pointer;
+ margin: 20px auto;
+ padding: 10px 65px;
+}
+
+.submitButton:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.register_text {
+ font-size: 1.2rem;
+ font-weight: 700;
+ color: $secondaryTextColor;
+}
+
+.login_link {
+ display: inline-block;
+ padding: 10px 75px;
+ background-color: $primaryBackgroundButton;
+ color: $secondaryTextColor;
+ font-size: 1.6rem;
+ font-weight: 700;
+ justify-self: center;
+ align-self: center;
+ margin-bottom: 20px;
+ border-radius: 0.5rem;
+
+ &:hover {
+ background-color: $primaryBackgroundButtonHover;
+ transition: background-color 0.3s ease;
+ color: $secondaryTextColor;
+ }
+
+ &:active {
+ background-color: $primaryBackgroundButtonActive;
+ }
+}
+
+.myLabel {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ font-size: 1.3rem;
+ color: hsla(0, 63%, 3%, 0.564);
+}
+
+.myInput {
+ outline: none;
+ width: 20px;
+}
+
+.myButton {
+ outline: none;
+ background-color: #d6b258;
+ cursor: pointer;
+}
+
+.myButton:hover {
+ background-color: #f0ac00;
+}
+
+.myButton:active {
+ background-color: #f0ca00;
+}
+
+.myInput_header {
+ position: relative;
+ top: 15px;
+ font-size: 1.4rem;
+ color: var(--primary-text-color);
+}
+
+@media (width < 650px) {
+ .registration {
+ input {
+ width: 300px;
+ }
+ }
+}
diff --git a/src/components/form/registration/RegistrationForm.tsx b/src/components/form/registration/RegistrationForm.tsx
new file mode 100644
index 0000000..ba6050a
--- /dev/null
+++ b/src/components/form/registration/RegistrationForm.tsx
@@ -0,0 +1,326 @@
+import React, { useEffect, useState, useContext } from 'react';
+import style from 'src/components/form/registration/RegistrationForm.module.scss';
+import { AddressForm } from 'src/components/address/Address.tsx';
+import { validateField } from 'src/components/validation/Validation.ts';
+import { validatePostalCode } from 'src/components/validation/PostalCodeValidation.ts';
+import { Country } from 'src/components/country/country.ts';
+import { Paragraph } from 'src/components/text/Text.tsx';
+import { Link } from 'src/components/link/Link.tsx';
+import { BaseAddress, CustomerDraft } from '@commercetools/platform-sdk';
+import { apiRoot } from 'src/services/api/ctpClient.ts';
+import { useNavigate } from 'react-router-dom';
+import { v4 as uuidv4 } from 'uuid';
+
+import { loginRequest } from 'src/services/api/loginRequest.ts';
+import { saveCredentials } from 'src/services/userData/saveEmailPassword.ts';
+import { createCustomer } from 'src/services/api/registrationCustomer.ts';
+import { ServerError } from 'src/utils/error/RequestErrors.ts';
+import { ModalWindow } from 'src/components/modalWindow/modalWindow.tsx';
+import { BillingAddressForm } from 'src/components/address/BillingAddress.tsx';
+
+import { customerModel, ICustomerModel } from 'src/model/Customer.ts';
+import { CurrentUserContext } from 'src/App.tsx';
+import { RegistrationMainFields } from './RegistrationMainFields.tsx';
+
+let countryShipping: Country;
+let countryBilling: Country;
+
+const requiredFields: (keyof ICustomerModel)[] = [
+ 'email',
+ 'password',
+ 'firstName',
+ 'lastName',
+ 'dateOfBirth',
+];
+
+export const RegistrationForm: React.FC = () => {
+ const context = useContext(CurrentUserContext);
+
+ if (!context) {
+ throw new Error('RegistrationForm must be used within a CurrentUserContext.Provider');
+ }
+
+ const { setCurrentUser } = context;
+ const [formData, setFormData] = useState(customerModel);
+
+ const [errors, setErrors] = useState(customerModel);
+
+ const [isFormValid, setIsFormValid] = useState(false);
+
+ const popupMessage = { status: '', message: '' };
+
+ const [modalData, setModalData] = useState(popupMessage);
+
+ const navigation = useNavigate();
+
+ countryShipping = Country[formData.country as keyof typeof Country];
+ countryBilling = Country[formData.billingCountry as keyof typeof Country];
+
+ const handleDefaultAddress = (checked: boolean) => {
+ setFormData({
+ ...formData,
+ isShippingDefaultAddress: checked,
+ });
+ };
+ const handleBillingAddress = (checked: boolean) => {
+ setFormData({
+ ...formData,
+ isBillingDefaultAddress: checked,
+ });
+ };
+
+ const validateOneField = (name: string, value: string) => {
+ const error = validateField(name, value, countryShipping, countryBilling);
+ const errorValidate = error === '' ? '' : error;
+ setErrors({
+ ...errors,
+ [name]: errorValidate,
+ });
+ };
+
+ const handleSameAddress = (checked: boolean) => {
+ setFormData((prevFormData) => ({
+ ...prevFormData,
+ isEqualAddress: checked,
+ ...(checked && {
+ billingStreet: prevFormData.streetName,
+ billingCity: prevFormData.city,
+ billingCountry: prevFormData.country,
+ billingPostalCode: prevFormData.postalCode,
+ }),
+ }));
+ };
+
+ useEffect(() => {
+ if (formData.isEqualAddress) {
+ const billingAddressFields: string[] = [
+ 'billingStreet',
+ 'billingCity',
+ 'billingCountry',
+ 'billingPostalCode',
+ ];
+ billingAddressFields.forEach((field) => {
+ if (typeof formData[field] === 'string') {
+ const value = formData[field] as string;
+ const error = validateField(field, value, countryShipping, countryBilling);
+ const errorValidate = error === '' ? '' : error;
+ setErrors((prevErrors) => ({
+ ...prevErrors,
+ [field]: errorValidate,
+ }));
+ }
+ });
+ }
+ }, [formData.isEqualAddress, formData]);
+
+ const handleChange = (event: React.ChangeEvent) => {
+ const { name, value } = event.target;
+ setFormData({
+ ...formData,
+ [name]: value,
+ });
+
+ validateOneField(name, value);
+ };
+
+ useEffect(() => {
+ const allFieldsValid = Object.values(errors).every(
+ (error) => error === '' || typeof error === 'boolean',
+ );
+ setIsFormValid(allFieldsValid);
+ }, [errors, formData]);
+
+ useEffect(() => {
+ const error = validatePostalCode(countryShipping, formData.postalCode);
+ setErrors((prevErrors) => ({
+ ...prevErrors,
+ postalCode: error,
+ }));
+ }, [formData.country, formData.postalCode]);
+
+ useEffect(() => {
+ const error = validatePostalCode(countryBilling, formData.billingPostalCode);
+ setErrors((prevErrors) => ({
+ ...prevErrors,
+ billingPostalCode: error,
+ }));
+ }, [formData.billingCountry, formData.billingPostalCode]);
+
+ useEffect(() => {
+ if (modalData.status) {
+ const timer = setTimeout(() => {
+ setModalData({ status: '', message: '' });
+ }, 4000);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }
+ return () => {
+ ('');
+ };
+ }, [modalData]);
+
+ const handleSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+
+ const isAnyEmpty = requiredFields.some((field) => !formData[field]);
+
+ if (!isAnyEmpty && isFormValid) {
+ let generatedCustomerID: string;
+ let generatedShippAddrID: string | undefined;
+ let generatedBillAddrID: string | undefined;
+ const generateUUID = (): string => {
+ return uuidv4();
+ };
+
+ const addresses: BaseAddress[] = [
+ {
+ id: generateUUID(),
+ streetName: formData.streetName,
+ city: formData.city,
+ country: Country[formData.country as keyof typeof Country],
+ postalCode: formData.postalCode,
+ },
+ {
+ id: generateUUID(),
+ streetName: formData.billingStreet,
+ city: formData.billingCity,
+ country: Country[formData.billingCountry as keyof typeof Country],
+ postalCode: formData.billingPostalCode,
+ },
+ ];
+
+ const newCustomer: CustomerDraft = {
+ key: generateUUID(),
+ email: formData.email,
+ password: formData.password,
+ firstName: formData.firstName,
+ lastName: formData.lastName,
+ dateOfBirth: formData.dateOfBirth,
+ addresses,
+ ...(formData.isShippingDefaultAddress && { defaultShippingAddress: 0 }),
+ ...(formData.isBillingDefaultAddress && { defaultBillingAddress: 1 }),
+ };
+
+ createCustomer(newCustomer)
+ .then(async ({ body }) => {
+ generatedCustomerID = body.customer.id;
+ generatedShippAddrID = body.customer.addresses[0].id;
+ generatedBillAddrID = body.customer.addresses[1].id;
+ if (body.customer.email) {
+ if (formData.password) {
+ saveCredentials(formData.email, formData.password);
+ setCurrentUser({ ...body.customer });
+ await loginRequest(formData.email, formData.password);
+ }
+
+ setTimeout(() => {
+ navigation('/');
+ }, 1000);
+ setModalData({ status: 'Success', message: 'You have been registered.' });
+ }
+ })
+ .catch((error: unknown) => {
+ if (error instanceof ServerError) {
+ setModalData({ status: 'Error', message: error.message });
+ }
+ });
+
+ if (formData.isShippingDefaultAddress) {
+ const setDefualtAdd = () => {
+ return apiRoot
+ .customers()
+ .withId({ ID: generatedCustomerID })
+ .post({
+ body: {
+ version: 1,
+ actions: [
+ {
+ action: 'setDefaultShippingAddress',
+ addressId: generatedShippAddrID,
+ },
+ ],
+ },
+ })
+ .execute();
+ };
+ setDefualtAdd()
+ .then(() => {
+ // TODO
+ })
+ .catch((error: unknown) => {
+ if (error) {
+ // TODO
+ }
+ });
+ }
+ if (formData.isBillingDefaultAddress) {
+ const setDefualtAdd = () => {
+ return apiRoot
+ .customers()
+ .withId({ ID: generatedCustomerID })
+ .post({
+ body: {
+ version: 1,
+ actions: [
+ {
+ action: 'setDefaultBillingAddress',
+ addressId: generatedBillAddrID,
+ },
+ ],
+ },
+ })
+ .execute();
+ };
+ setDefualtAdd()
+ .then(() => {
+ // TODO
+ })
+ .catch((error: unknown) => {
+ if (error) {
+ // TODO
+ }
+ });
+ }
+ }
+ };
+
+ return (
+ <>
+
+ {modalData.message && }
+ >
+ );
+};
+
+export default RegistrationForm;
diff --git a/src/components/form/registration/RegistrationMainFields.tsx b/src/components/form/registration/RegistrationMainFields.tsx
new file mode 100644
index 0000000..89c151d
--- /dev/null
+++ b/src/components/form/registration/RegistrationMainFields.tsx
@@ -0,0 +1,109 @@
+import React from 'react';
+import styleAddr from 'src/components/address/Address.module.scss';
+
+import { InputWithLabel } from 'src/components/input/InputWithLabel.tsx';
+
+export interface RegistrationMainFieldsProps {
+ formData: {
+ email: string | undefined;
+ password: string | undefined;
+ firstName: string | undefined;
+ lastName: string | undefined;
+ dateOfBirth: string | undefined;
+ };
+ handleChange: (event: React.ChangeEvent) => void;
+ errors: {
+ email: string | undefined;
+ password: string | undefined;
+ firstName: string | undefined;
+ lastName: string | undefined;
+ dateOfBirth: string | undefined;
+ };
+ showFields?: {
+ email: boolean;
+ password: boolean;
+ firstName: boolean;
+ lastName: boolean;
+ dateOfBirth: boolean;
+ };
+ disabledMode?: boolean;
+}
+
+export const RegistrationMainFields: React.FC = ({
+ formData,
+ handleChange,
+ errors,
+ showFields = { email: true, password: true, firstName: true, lastName: true, dateOfBirth: true },
+ disabledMode = false,
+}) => {
+ return (
+
+ {showFields.email && (
+
+ )}
+ {showFields.password && (
+
+ )}
+
+ {showFields.firstName && (
+
+ )}
+ {showFields.lastName && (
+
+ )}
+ {showFields.dateOfBirth && (
+
+ )}
+
+ );
+};
diff --git a/src/components/header/Header.module.scss b/src/components/header/Header.module.scss
new file mode 100644
index 0000000..b1cff48
--- /dev/null
+++ b/src/components/header/Header.module.scss
@@ -0,0 +1,120 @@
+@import 'src/styles/variables';
+@import 'src/styles/mixins';
+
+.header,
+.navigation {
+ display: flex;
+ justify-content: space-between;
+}
+
+.header {
+ background-color: rgba(255, 255, 255);
+ position: sticky;
+ top: 0;
+ width: 100%;
+
+ &.hidden {
+ display: none;
+ }
+}
+
+.container {
+ display: flex;
+ height: 100%;
+ width: 90%;
+ margin: 0 auto;
+ justify-content: space-between;
+
+ @include media-600 {
+ flex-direction: column;
+ }
+}
+
+.navigation {
+ width: 27%;
+ align-items: end;
+
+ @include media-1200 {
+ width: 32%;
+ }
+
+ @include media-1000 {
+ width: 40%;
+ }
+
+ @include media-800 {
+ width: 55%;
+ }
+
+ @include media-600 {
+ width: 63%;
+ }
+
+ @include media-500 {
+ width: 80%;
+ }
+
+ @include media-400 {
+ width: 90%;
+ }
+}
+
+.login_container {
+ display: flex;
+ &.hidden {
+ display: none;
+ }
+}
+
+.logout_container {
+ display: flex;
+}
+
+.logo,
+.logout,
+.link {
+ color: $secondaryTextColor;
+ &:hover {
+ cursor: pointer;
+ transition-duration: 1s;
+ }
+}
+
+.logo {
+ font-size: 3.2rem;
+ &.inactive:hover {
+ cursor: auto;
+ color: $secondaryTextColor;
+ }
+ @include media-600 {
+ margin-left: 5px;
+ }
+}
+
+.logout {
+ &:hover {
+ color: var(--hover-text-color);
+ cursor: pointer;
+ }
+}
+
+.link,
+.logout {
+ display: flex;
+ height: 100%;
+ font-size: 1.6rem;
+ align-items: flex-end;
+ padding: 5px 10px 0 10px;
+}
+
+.link.active,
+.logout.active {
+ color: $secondaryActiveLink;
+ text-decoration: underline;
+ transition-duration: 1s;
+
+ &:hover {
+ color: $secondaryActiveLink;
+ cursor: auto;
+ }
+}
diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx
new file mode 100644
index 0000000..94b9a35
--- /dev/null
+++ b/src/components/header/Header.tsx
@@ -0,0 +1,101 @@
+import React, { useEffect, useState } from 'react';
+import styles from 'src/components/header/Header.module.scss';
+import { Link } from 'components/link/Link.tsx';
+import { useLocation, useNavigate } from 'react-router-dom';
+import { Form } from 'src/components/form/form.tsx';
+
+const links = [
+ {
+ to: '/login',
+ title: 'LOG IN',
+ id: 1,
+ },
+ {
+ to: '/register',
+ title: 'REGISTER',
+ id: 2,
+ },
+];
+
+export const Header: React.FC = () => {
+ const location = useLocation().pathname;
+ const navigation = useNavigate();
+ const [activeLink, setActiveLink] = useState(location);
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
+
+ useEffect(() => {
+ setActiveLink(location);
+ const user = localStorage.getItem('userTokens');
+ setIsLoggedIn(!!user);
+ }, [location]);
+
+ useEffect(() => {
+ const user = localStorage.getItem('userTokens');
+ if (user && window.location.pathname === '/login') {
+ navigation('/');
+ }
+ if (!user && window.location.pathname === '/profile') {
+ navigation('/login');
+ }
+ }, [navigation]);
+
+ const clearLocalStorage = () => {
+ localStorage.clear();
+ };
+
+ const handelLogout = () => {
+ clearLocalStorage();
+ setIsLoggedIn(false);
+ Form();
+ };
+ const isProductPage = location.startsWith('/product/');
+ const is404Page =
+ !isProductPage &&
+ location !== '/' &&
+ location !== '/login' &&
+ location !== '/register' &&
+ location !== '/catalog' &&
+ location !== '/profile';
+ const isHeaderInactive = location === '/';
+ const isToken = localStorage.getItem('userTokens');
+ return (
+
+ );
+};
diff --git a/src/components/input/InputWithLabel.tsx b/src/components/input/InputWithLabel.tsx
new file mode 100644
index 0000000..a6e1553
--- /dev/null
+++ b/src/components/input/InputWithLabel.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import style from 'src/components/address/Address.module.scss';
+
+interface InputProps {
+ id: string;
+ type: string;
+ name: string;
+ label: string;
+ value: string | undefined;
+ onChange: (event: React.ChangeEvent) => void;
+ required?: boolean;
+ error?: string;
+ className?: string;
+ disabledMode?: boolean;
+}
+
+export const InputWithLabel: React.FC = ({
+ id,
+ type,
+ name,
+ label,
+ value,
+ onChange,
+ required = false,
+ error,
+ className,
+ disabledMode = false,
+}) => {
+ return (
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+};
diff --git a/src/components/layout/Layout.module.scss b/src/components/layout/Layout.module.scss
new file mode 100644
index 0000000..ea0a514
--- /dev/null
+++ b/src/components/layout/Layout.module.scss
@@ -0,0 +1,11 @@
+@import 'src/styles/variables';
+.layout {
+ width: inherit;
+ background-image: url('../../public/main_image2.jpg');
+ background-repeat: no-repeat;
+ background-size: cover;
+ display: flex;
+ background-position: center;
+ width: 100%;
+ height: 70vh;
+}
diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx
new file mode 100644
index 0000000..6946363
--- /dev/null
+++ b/src/components/layout/Layout.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+
+interface LayoutProps {
+ children: React.ReactNode;
+ className: string;
+}
+
+export const Layout: React.FC = ({ children, className }) => {
+ return {children}
;
+};
diff --git a/src/components/link/Link.tsx b/src/components/link/Link.tsx
new file mode 100644
index 0000000..a971d8b
--- /dev/null
+++ b/src/components/link/Link.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { NavLink } from 'react-router-dom';
+
+interface LinkProps {
+ to: string;
+ title?: string;
+ className: string;
+ children?: React.ReactNode;
+ id?: string;
+ onClick?: () => void;
+}
+
+export const Link: React.FC = ({ to, title, className, id, children, onClick }) => {
+ return (
+
+ {title}
+ {children}
+
+ );
+};
diff --git a/src/components/modalWindow/modalImage.module.scss b/src/components/modalWindow/modalImage.module.scss
new file mode 100644
index 0000000..990e4b6
--- /dev/null
+++ b/src/components/modalWindow/modalImage.module.scss
@@ -0,0 +1,35 @@
+@import 'src/styles/variables';
+@import 'src/styles/mixins';
+
+.modal {
+ width: 80%;
+ height: 80vh;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -55%);
+ z-index: 10;
+}
+
+.modal_content {
+ display: flex;
+ width: 100%;
+ margin: 0 auto;
+ @include media-600 {
+ width: 80%;
+ align-self: center;
+ }
+}
+
+.close_container {
+ width: 36px;
+ height: 36px;
+ &:hover {
+ cursor: pointer;
+ }
+}
+
+.modal_close {
+ display: block;
+ width: 36px;
+}
diff --git a/src/components/modalWindow/modalImage.tsx b/src/components/modalWindow/modalImage.tsx
new file mode 100644
index 0000000..915735b
--- /dev/null
+++ b/src/components/modalWindow/modalImage.tsx
@@ -0,0 +1,30 @@
+import React, { ReactNode } from 'react';
+import style from 'src/components/modalWindow/modalImage.module.scss';
+import closeIcon from 'src/public/icons8-close-64.png';
+
+interface ModalProps {
+ children: ReactNode;
+ closeModalWindow: () => void;
+}
+export const Modal: React.FC = ({ children, closeModalWindow }) => {
+ return (
+
+
+ {children}
+
{
+ if (event.key === 'Enter' || event.key === ' ') {
+ closeModalWindow();
+ }
+ }}
+ >
+

+
+
+
+ );
+};
diff --git a/src/components/modalWindow/modalWindow.module.scss b/src/components/modalWindow/modalWindow.module.scss
new file mode 100644
index 0000000..da62d57
--- /dev/null
+++ b/src/components/modalWindow/modalWindow.module.scss
@@ -0,0 +1,29 @@
+.parent {
+ width: 100%;
+ height: 95%;
+ position: fixed;
+ display: flex;
+ justify-content: flex-end;
+ align-items: flex-start;
+ top: 50px;
+ left: -10px;
+}
+
+.children {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ border-radius: 10px;
+ font-size: 16px;
+ padding: 10px;
+ background-color: rgba(94, 94, 94, 0.192);
+}
+
+.children.error {
+ background-color: red;
+}
+
+.children.positive {
+ background-color: green;
+}
diff --git a/src/components/modalWindow/modalWindow.tsx b/src/components/modalWindow/modalWindow.tsx
new file mode 100644
index 0000000..5e4e595
--- /dev/null
+++ b/src/components/modalWindow/modalWindow.tsx
@@ -0,0 +1,31 @@
+import styles from 'src/components/modalWindow/modalWindow.module.scss';
+import { IResponse } from 'src/components/tempFolderForDevelop/responseHandler.ts';
+import PropTypes from 'prop-types';
+import { ErrorType } from 'src/utils/error/RequestErrors.ts';
+
+interface ModalWindowProps {
+ data: IResponse;
+}
+
+const ModalWindow: React.FC = ({ data }) => {
+ const { status, message } = data;
+ return (
+
+
+ {status}
+ {message}
+
+
+ );
+};
+
+ModalWindow.propTypes = {
+ data: PropTypes.shape({
+ status: PropTypes.string.isRequired,
+ message: PropTypes.string.isRequired,
+ }).isRequired,
+};
+
+export { ModalWindow };
diff --git a/src/components/router/Router.tsx b/src/components/router/Router.tsx
new file mode 100644
index 0000000..2b39801
--- /dev/null
+++ b/src/components/router/Router.tsx
@@ -0,0 +1,26 @@
+import { RegistrationPage } from 'src/logic/registrationPage/registrationPage.tsx';
+import { Main } from 'src/logic/mainPage/MainPage.tsx';
+import { Login } from 'src/logic/loginPage/LoginPage.tsx';
+import { Error } from 'src/logic/errorPage/ErrorPage.tsx';
+import { Catalog } from 'src/logic/catalogPage/CatalogPage.tsx';
+import { UserProfilePage } from 'src/logic/userProfilePage/UserProfilePage.tsx';
+import { Product } from 'src/logic/productPage/Product.tsx';
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
+import { Header } from 'src/components/header/Header.tsx';
+
+export const Router = () => {
+ return (
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+};
diff --git a/src/components/search/Search.module.scss b/src/components/search/Search.module.scss
new file mode 100644
index 0000000..c50b06a
--- /dev/null
+++ b/src/components/search/Search.module.scss
@@ -0,0 +1,14 @@
+.search_container {
+ display: flex;
+ margin-top: 10px;
+ justify-content: center;
+}
+
+form {
+ display: flex;
+}
+
+.input_search,
+.button_input {
+ border-radius: 0px;
+}
diff --git a/src/components/search/Search.tsx b/src/components/search/Search.tsx
new file mode 100644
index 0000000..2ca42f7
--- /dev/null
+++ b/src/components/search/Search.tsx
@@ -0,0 +1,36 @@
+import React, { useState } from 'react';
+import style from 'src/components/search/Search.module.scss';
+
+interface SearchComponentProps {
+ onSearch: (query: string) => void;
+}
+
+export const SearchComponent: React.FC = ({ onSearch }) => {
+ const [query, setQuery] = useState('');
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ setQuery(event.target.value);
+ };
+
+ const handleSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+ onSearch(query);
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/src/components/selector/Selector.tsx b/src/components/selector/Selector.tsx
new file mode 100644
index 0000000..38cc99b
--- /dev/null
+++ b/src/components/selector/Selector.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+
+interface Option {
+ value: string | undefined;
+ label: string;
+}
+
+interface SelectorProps {
+ id: string;
+ name: string;
+ value: string | undefined;
+ label: string;
+ options: Option[];
+ onChange: React.ChangeEventHandler;
+ onBlur: React.ChangeEventHandler;
+ error?: string;
+ disabledMode?: boolean;
+}
+
+const Selector: React.FC<{ selectorProps: SelectorProps }> = ({ selectorProps }) => {
+ const {
+ id,
+ name,
+ value,
+ label,
+ options,
+ onChange,
+ onBlur,
+ error,
+ disabledMode = false,
+ } = selectorProps;
+
+ return (
+
+
+
+
+ {error &&
{error}
}
+
+ );
+};
+
+export default Selector;
diff --git a/src/components/sort/Sort.module.scss b/src/components/sort/Sort.module.scss
new file mode 100644
index 0000000..bbf1f1f
--- /dev/null
+++ b/src/components/sort/Sort.module.scss
@@ -0,0 +1,18 @@
+.sort_container {
+ display: flex;
+ align-items: center;
+ margin-top: 20px;
+}
+
+.form_container {
+ display: flex;
+ flex-direction: column;
+ font-size: 16px;
+ width: 100%;
+}
+
+.select_sort {
+ margin-right: 10px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
diff --git a/src/components/sort/Sort.tsx b/src/components/sort/Sort.tsx
new file mode 100644
index 0000000..54a974a
--- /dev/null
+++ b/src/components/sort/Sort.tsx
@@ -0,0 +1,39 @@
+import React, { useState } from 'react';
+import style from 'src/components/sort/Sort.module.scss';
+
+interface SortComponentProps {
+ onSort: (criteria: string) => void;
+ defaultCriteria?: string;
+}
+
+export const SortComponent: React.FC = ({
+ onSort,
+ defaultCriteria = 'price',
+}) => {
+ const [criteria, setCriteria] = useState(defaultCriteria);
+
+ const handleCriteriaChange = (event: React.ChangeEvent) => {
+ const newCriteria = event.target.value;
+ setCriteria(newCriteria);
+ onSort(newCriteria);
+ };
+
+ const handleSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+ onSort(criteria);
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/src/components/tempFolderForDevelop/responseHandler.ts b/src/components/tempFolderForDevelop/responseHandler.ts
new file mode 100644
index 0000000..167ac73
--- /dev/null
+++ b/src/components/tempFolderForDevelop/responseHandler.ts
@@ -0,0 +1,14 @@
+export interface IResponse {
+ status: string;
+ message: string;
+}
+
+export const myRedirect = (res: IResponse) => {
+ if (res.status === 'Success') {
+ return 'Success';
+ }
+ if (res.status === 'Invalid') {
+ return 'Invalid';
+ }
+ return '';
+};
diff --git a/src/components/tempFolderForDevelop/statusHandler.ts b/src/components/tempFolderForDevelop/statusHandler.ts
new file mode 100644
index 0000000..5cc744c
--- /dev/null
+++ b/src/components/tempFolderForDevelop/statusHandler.ts
@@ -0,0 +1,17 @@
+export const myStatus = (status: boolean, message: string) => {
+ const obj = {
+ status: '',
+ message: '',
+ };
+
+ if (status) {
+ obj.status = 'Success';
+ obj.message = message;
+ // obj.message = 'Login successful!';
+ } else {
+ obj.status = 'Invalid';
+ obj.message = message;
+ // obj.message = 'Incorrect email or password!';
+ }
+ return obj;
+};
diff --git a/src/components/text/Text.tsx b/src/components/text/Text.tsx
new file mode 100644
index 0000000..b626581
--- /dev/null
+++ b/src/components/text/Text.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+
+type Tags = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span';
+
+interface ParagraphProps {
+ tag: Tags;
+ title: string;
+ className: string;
+}
+
+export const Paragraph: React.FC = ({ tag, title, className }) => {
+ return React.createElement(
+ tag,
+ {
+ className,
+ },
+ title,
+ );
+};
diff --git a/src/components/validation/PostalCodeValidation.ts b/src/components/validation/PostalCodeValidation.ts
new file mode 100644
index 0000000..095dd4d
--- /dev/null
+++ b/src/components/validation/PostalCodeValidation.ts
@@ -0,0 +1,42 @@
+import { Country } from '../country/country.ts';
+
+export const validatePostalCode = (
+ country: Country,
+ postalCodeParam: string | undefined,
+): string => {
+ let pattern: RegExp;
+ let error = '';
+ let postalCode;
+
+ if (!postalCodeParam) {
+ postalCode = '';
+ } else {
+ postalCode = postalCodeParam;
+ }
+
+ switch (country) {
+ case Country.France:
+ pattern = /^\d{2}[ ]?\d{3}$/;
+ if (!pattern.test(postalCode)) {
+ error = '2 Correct formats:<2numbers space 3numbers> <5 numbers> ';
+ }
+ break;
+ case Country.Germany:
+ case Country.Italy:
+ pattern = /^[\d]{5}$/;
+ if (!pattern.test(postalCode)) {
+ error = 'Correct format: 5 numbers';
+ }
+ break;
+ case Country.Netherlands:
+ pattern = /^\d{4}\s?[A-Za-z]{2}$/;
+ if (!pattern.test(postalCode)) {
+ error = 'Correct format: <4 numbers><2 letters>';
+ }
+ break;
+ default:
+ return '';
+ }
+
+ return pattern.test(postalCode) ? '' : error;
+};
diff --git a/src/components/validation/Validation.ts b/src/components/validation/Validation.ts
new file mode 100644
index 0000000..18611cb
--- /dev/null
+++ b/src/components/validation/Validation.ts
@@ -0,0 +1,128 @@
+// validation rules are compatible with CommerceTools
+// https://docs.commercetools.com/api/projects/customers
+
+import { Country } from '../country/country.ts';
+import { validatePostalCode } from './PostalCodeValidation.ts';
+
+export const validateEmail = (value: string) => {
+ const errorMessages: string[] = [];
+
+ if (value.startsWith('@')) {
+ errorMessages.push('Cannot start with @');
+ }
+ if (!/^[^.]/.test(value)) {
+ errorMessages.push('Cannot start with a dot');
+ }
+ if (!/^\S*$/.test(value)) {
+ errorMessages.push('Cannot contain spaces');
+ }
+ if (!/^[a-zA-Z0-9@.]+$/.test(value)) {
+ errorMessages.push('Can only contain English letters or digits');
+ }
+ if (!value.includes('@')) {
+ errorMessages.push('Must contain @');
+ }
+ if (!/^\S+@\S+\.\S+$/.test(value)) {
+ errorMessages.push('Must contain a domain name like example.com');
+ }
+ const dotMatches = value.match(/\./g);
+ if (!(dotMatches !== null && dotMatches.length === 1)) {
+ errorMessages.push('Must contain only one dot');
+ }
+ if (!/^[^@\s]+@[^.@\s]+\.[^@\s]+$/.test(value)) {
+ errorMessages.push('Must be in the correct format like user@example.com');
+ }
+ return errorMessages.join('\n');
+};
+
+export const validatePassword = (password: string): string => {
+ const errorMessages: string[] = [];
+
+ if (password.length < 8) {
+ errorMessages.push('Minimum 8 characters required');
+ }
+ if (!/[a-z]/.test(password)) {
+ errorMessages.push('At least 1 lowercase letter required');
+ }
+ if (!/[A-Z]/.test(password)) {
+ errorMessages.push('At least 1 uppercase letter required');
+ }
+ if (!/\d/.test(password)) {
+ errorMessages.push('At least 1 digit required');
+ }
+ if (/\s/.test(password)) {
+ errorMessages.push('Spaces are not allowed');
+ }
+
+ return errorMessages.join('\n');
+};
+
+const validateName = (name: string): string => {
+ const emailRegex = /^[a-zA-Z]+$/;
+ return emailRegex.test(name) ? '' : 'Must contain at least 1 letter and use only Latin letters.';
+};
+
+const validateBirthday = (value: string): string => {
+ const birthday = new Date(value);
+ const currentDate = new Date();
+ const currentYear = currentDate.getFullYear();
+ const minDate = new Date(currentYear - 110, currentDate.getMonth(), currentDate.getDate());
+ const maxDate = new Date(currentYear - 15, currentDate.getMonth(), currentDate.getDate());
+
+ let result = '';
+ if (birthday < minDate || birthday > maxDate) {
+ result = 'Age shold be more then 15 yo';
+ }
+
+ return result;
+};
+
+const validateNonEmpty = (value: string): string => {
+ const trimmedValue = value.trim();
+ if (!trimmedValue) {
+ return 'Must contain at least one character';
+ }
+ return '';
+};
+
+const validateCountry = (value: string): string => {
+ if (!(value in Country)) {
+ return 'Please select a valid country.';
+ }
+ return '';
+};
+
+export const validateField = (
+ name: string,
+ inputValue: string,
+ countryShipping: Country,
+ countryBilling: Country,
+): string => {
+ const validateValue: Country | string = inputValue;
+
+ switch (name) {
+ case 'email':
+ return validateEmail(validateValue);
+ case 'password':
+ return validatePassword(validateValue);
+ case 'firstName':
+ case 'lastName':
+ case 'city':
+ case 'billingCity':
+ return validateName(validateValue);
+ case 'dateOfBirth':
+ return validateBirthday(validateValue);
+ case 'streetName':
+ case 'billingStreet':
+ return validateNonEmpty(validateValue);
+ case 'postalCode':
+ return validatePostalCode(countryShipping, validateValue);
+ case 'billingPostalCode':
+ return validatePostalCode(countryBilling, validateValue);
+ case 'country':
+ case 'billingCountry':
+ return validateCountry(validateValue);
+ default:
+ return '';
+ }
+};
diff --git a/src/index.scss b/src/index.scss
new file mode 100644
index 0000000..a02f3ae
--- /dev/null
+++ b/src/index.scss
@@ -0,0 +1,54 @@
+@import 'src/styles/normalize';
+@import 'src/styles/variables';
+
+@font-face {
+ font-family: 'Open Sans', sans-serif;
+ src: url(src/public/OpenSans-Regular.ttf) format('truetype');
+}
+
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ font-family: 'Open Sans', sans-serif;
+ font-weight: 400;
+ font-size: 10px;
+ background-color: var(--background-color);
+ color: var(--primary-text-color);
+
+ --background-color: #efe6d3;
+ --primary-text-color: hsla(0, 63%, 3%, 0.564);
+ --error-text: rgba(233, 49, 49, 0.769);
+ --register-button: #2db942;
+ --hover-text-color: #f5861f;
+}
+
+#root {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ height: 100vh;
+}
+
+a {
+ color: var(--primary-text-color);
+ text-decoration: none;
+}
+
+a:hover {
+ color: var(--hover-text-color);
+}
+
+input,
+select,
+button {
+ max-width: 400px;
+ border-radius: 5px;
+ border: none;
+ font-size: 2rem;
+}
+
+h2 {
+ font-size: 1.9em;
+}
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..9a950c1
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,14 @@
+import { createRoot } from 'react-dom/client';
+import React from 'react';
+import { App } from 'src/App.tsx';
+import 'src/index.scss';
+
+const rootElement = document.createElement('div');
+rootElement.id = 'root';
+document.body.appendChild(rootElement);
+
+createRoot(rootElement).render(
+
+
+ ,
+);
diff --git a/src/logic/catalogPage/CatalogPage.module.scss b/src/logic/catalogPage/CatalogPage.module.scss
new file mode 100644
index 0000000..a9e5ea5
--- /dev/null
+++ b/src/logic/catalogPage/CatalogPage.module.scss
@@ -0,0 +1,18 @@
+.layout {
+ width: 90%;
+ display: flex;
+ flex-direction: row;
+ margin: 0 auto;
+ align-items: flex-start;
+ justify-content: center;
+}
+
+.test {
+ display: flex;
+ flex-direction: column;
+ width: 20%;
+}
+
+.dots {
+ height: 5px;
+}
diff --git a/src/logic/catalogPage/CatalogPage.tsx b/src/logic/catalogPage/CatalogPage.tsx
new file mode 100644
index 0000000..ee98c01
--- /dev/null
+++ b/src/logic/catalogPage/CatalogPage.tsx
@@ -0,0 +1,292 @@
+import React, { useEffect, useState } from 'react';
+import style from 'src/logic/catalogPage/CatalogPage.module.scss';
+import { Card } from 'src/components/cards/Cards.tsx';
+import { Filter } from 'src/components/filter/Filter.tsx';
+import { filterTools } from 'src/services/tools/filterTools.ts';
+import {
+ fetchAllProducts,
+ fetchCategory,
+ fetchColorProducts,
+ fetchPriceProducts,
+ fetchSearchProducts,
+ fetchSizeProducts,
+ fetchSortNameProducts,
+ fetchSortPriceProducts,
+} from 'src/services/api/filterRequests.ts';
+import { ProductProjection } from '@commercetools/platform-sdk';
+import { SearchComponent } from 'src/components/search/Search.tsx';
+import { SortComponent } from 'src/components/sort/Sort.tsx';
+import { CategoryComponent } from 'src/components/category/Category.tsx';
+
+const initializeFilters = async () => {
+ const [colors, sizes, prices] = await filterTools();
+ return { colors, sizes, prices };
+};
+
+interface FilterOption {
+ label: string;
+ value: string;
+}
+
+interface Filters {
+ colors: FilterOption[];
+ sizes: FilterOption[];
+ prices: FilterOption[];
+}
+
+export const Catalog: React.FC = () => {
+ const [defaultColor, setDefaultColor] = useState([]);
+ const [productState, setProductState] = useState([]);
+ const [colorsArray, setColorsArray] = useState([]);
+ const [sizesArray, setSizesArray] = useState([]);
+ const [pricesArray, setPricesArray] = useState([]);
+ const [errorState, setErrorState] = useState(null);
+ const [selectedCategory, setSelectedCategory] = useState('All category');
+
+ const [filters, setFilters] = useState({
+ colors: [],
+ sizes: [],
+ prices: [],
+ });
+
+ useEffect(() => {
+ const getProducts = async () => {
+ try {
+ const result = await fetchAllProducts();
+ setDefaultColor(result);
+ setProductState(result);
+ } catch (error: unknown) {
+ setErrorState('Error fetching products');
+ }
+ };
+ getProducts().catch(() => {
+ setErrorState('Error fetching products');
+ });
+
+ const initialize = async () => {
+ try {
+ const { colors, sizes, prices } = await initializeFilters();
+ setFilters({ colors, sizes, prices });
+ } catch (error: unknown) {
+ setErrorState('Error initializing filters');
+ }
+ };
+ initialize().catch(() => {
+ setErrorState('Error initializing filters');
+ });
+ }, []);
+
+ const handleChange = (event: React.ChangeEvent): void => {
+ setSelectedCategory('All category');
+ const { name: category, value } = event.target;
+
+ const handleAsyncChange = async () => {
+ if (event.target.checked) {
+ if (category === 'color') {
+ const updatedColorsArray = [...colorsArray, value];
+ setColorsArray(updatedColorsArray);
+ try {
+ const result = await fetchColorProducts(updatedColorsArray, sizesArray, pricesArray);
+ setProductState(result);
+ } catch (error: unknown) {
+ setErrorState('Error fetching color products');
+ }
+ }
+ if (category === 'size') {
+ const updatedSizesArray = [...sizesArray, value];
+ setSizesArray(updatedSizesArray);
+ try {
+ const result = await fetchSizeProducts(updatedSizesArray, colorsArray, pricesArray);
+ setProductState(result);
+ } catch (error: unknown) {
+ setErrorState('Error fetching size products');
+ }
+ }
+ if (category === 'price') {
+ const getPrice = async (priceRange: string) => {
+ const updatedPricesArray = [value];
+ setPricesArray(updatedPricesArray);
+ try {
+ const result = await fetchPriceProducts(priceRange, colorsArray, sizesArray);
+ setProductState(result);
+ } catch (error: unknown) {
+ setErrorState('Error fetching price products');
+ }
+ };
+
+ if (value === 'below 75') {
+ await getPrice('0 to 7500');
+ } else if (value === '75 to 125') {
+ await getPrice('7500 to 12500');
+ } else if (value === '125 to 200') {
+ await getPrice('12500 to 20000');
+ } else if (value === '200 to 250') {
+ await getPrice('20000 to 25000');
+ } else if (value === 'above 250') {
+ await getPrice('25000 to 100000000000000');
+ }
+ }
+ } else {
+ if (category === 'color') {
+ const updatedColorsArray = colorsArray.filter((color) => color !== value);
+ setColorsArray(updatedColorsArray);
+ try {
+ const result =
+ updatedColorsArray.length >= 0
+ ? await fetchColorProducts(updatedColorsArray, sizesArray, pricesArray)
+ : defaultColor;
+ setProductState(result);
+ } catch (error: unknown) {
+ setErrorState('Error fetching color products');
+ }
+ }
+ if (category === 'size') {
+ const updatedSizesArray = sizesArray.filter((size) => size !== value);
+ setSizesArray(updatedSizesArray);
+ try {
+ const result =
+ updatedSizesArray.length >= 0
+ ? await fetchSizeProducts(updatedSizesArray, colorsArray, pricesArray)
+ : defaultColor;
+ setProductState(result);
+ } catch (error: unknown) {
+ setErrorState('Error fetching size products');
+ }
+ }
+ if (category === 'price') {
+ const updatedPricesArray = pricesArray.filter((price) => price !== value);
+ setPricesArray(updatedPricesArray);
+ setProductState(defaultColor);
+ }
+ }
+ };
+
+ handleAsyncChange().catch(() => {
+ setErrorState('Error handling change');
+ });
+ };
+
+ const handleReset = (products: ProductProjection[]) => {
+ setProductState(products);
+ setColorsArray([]);
+ setSizesArray([]);
+ setPricesArray([]);
+ };
+
+ const handleSearch = (query: string) => {
+ setSelectedCategory('All category');
+ const fetchAndSetProducts = async () => {
+ try {
+ const searchProducts = await fetchSearchProducts(query);
+ setProductState(searchProducts);
+ } catch (error: unknown) {
+ setErrorState('Error searching products');
+ }
+ };
+
+ fetchAndSetProducts().catch(() => {
+ setErrorState('Error handling change');
+ });
+ };
+
+ const handleSort = (criteria: string) => {
+ setSelectedCategory('All category');
+ if (criteria === 'price high') {
+ const fetchAndSetProducts = async () => {
+ try {
+ const sortPriceProducts = await fetchSortPriceProducts('desc');
+ setProductState(sortPriceProducts);
+ } catch (error: unknown) {
+ setErrorState('Error searching products');
+ }
+ };
+
+ fetchAndSetProducts().catch(() => {
+ setErrorState('Error handling change');
+ });
+ }
+ if (criteria === 'price low') {
+ const fetchAndSetProducts = async () => {
+ try {
+ const sortPriceProducts = await fetchSortPriceProducts('asc');
+ setProductState(sortPriceProducts);
+ } catch (error: unknown) {
+ setErrorState('Error searching products');
+ }
+ };
+
+ fetchAndSetProducts().catch(() => {
+ setErrorState('Error handling change');
+ });
+ }
+ if (criteria === 'name a-z') {
+ const fetchAndSetProducts = async () => {
+ try {
+ const sortNameProducts = await fetchSortNameProducts('asc');
+ setProductState(sortNameProducts);
+ } catch (error: unknown) {
+ setErrorState('Error searching products');
+ }
+ };
+
+ fetchAndSetProducts().catch(() => {
+ setErrorState('Error handling change');
+ });
+ }
+ if (criteria === 'name z-a') {
+ const fetchAndSetProducts = async () => {
+ try {
+ const sortNameProducts = await fetchSortNameProducts('desc');
+ setProductState(sortNameProducts);
+ } catch (error: unknown) {
+ setErrorState('Error searching products');
+ }
+ };
+
+ fetchAndSetProducts().catch(() => {
+ setErrorState('Error handling change');
+ });
+ }
+ };
+
+ const handleCategoryClick = (category: string) => {
+ setSelectedCategory(category);
+ const fetchAndSetCategory = async () => {
+ try {
+ const currentCategory = await fetchCategory(category);
+ setProductState(currentCategory);
+ } catch (error: unknown) {
+ setErrorState('Error searching products');
+ }
+ };
+
+ fetchAndSetCategory().catch(() => {
+ setErrorState('Error handling change');
+ });
+ };
+
+ return (
+ <>
+ {errorState && {errorState}
}
+
+
+
+ >
+ );
+};
diff --git a/src/logic/errorPage/ErrorPage.module.scss b/src/logic/errorPage/ErrorPage.module.scss
new file mode 100644
index 0000000..f1773b9
--- /dev/null
+++ b/src/logic/errorPage/ErrorPage.module.scss
@@ -0,0 +1,56 @@
+@import 'src/styles/variables';
+@import 'src/styles/mixins';
+
+.error_container {
+ background-image: url('../../public/error.jpg');
+ display: flex;
+ flex-direction: column;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+ width: 100%;
+ height: 100vh;
+ justify-content: center;
+ align-items: center;
+ gap: 20px;
+}
+
+.error_link,
+.error_text,
+.error_title {
+ color: $secondaryTextColor;
+}
+
+.error_title,
+.error_text {
+ text-align: center;
+}
+
+.error_title {
+ font-size: 6.4rem;
+ margin: 0;
+}
+
+.error_text {
+ font-size: 2.4rem;
+}
+
+.error_link {
+ display: inline-block;
+ padding: 10px 20px;
+ background-color: $primaryBackgroundButton;
+ border-radius: 10px;
+ font-size: 1.6rem;
+ justify-self: center;
+ align-self: center;
+
+ &:hover {
+ background-color: $primaryBackgroundButtonHover;
+ transition: background-color 0.3s ease;
+ color: $secondaryTextColor;
+ }
+
+ &:active {
+ background-color: $primaryBackgroundButtonActive;
+ }
+}
diff --git a/src/logic/errorPage/ErrorPage.tsx b/src/logic/errorPage/ErrorPage.tsx
new file mode 100644
index 0000000..374900c
--- /dev/null
+++ b/src/logic/errorPage/ErrorPage.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { Link } from 'src/components/link/Link.tsx';
+import { Layout } from 'src/components/layout/Layout.tsx';
+import { Paragraph } from 'src/components/text/Text.tsx';
+import style from 'src/logic/errorPage/ErrorPage.module.scss';
+
+export const Error: React.FC = () => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/src/logic/loginPage/LoginPage.tsx b/src/logic/loginPage/LoginPage.tsx
new file mode 100644
index 0000000..15ef5de
--- /dev/null
+++ b/src/logic/loginPage/LoginPage.tsx
@@ -0,0 +1,14 @@
+import { Form } from 'src/components/form/form.tsx';
+import styles from 'src/logic/loginPage/loginPage.module.scss';
+
+const Login = () => {
+ return (
+
+ );
+};
+
+export { Login };
diff --git a/src/logic/loginPage/loginPage.module.scss b/src/logic/loginPage/loginPage.module.scss
new file mode 100644
index 0000000..12e2609
--- /dev/null
+++ b/src/logic/loginPage/loginPage.module.scss
@@ -0,0 +1,74 @@
+@import '../../styles/mixins';
+@import 'src/styles/variables';
+
+.container_login {
+ display: flex;
+ background-image: url('../../public/login-bg.jpeg');
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+ width: 100%;
+ height: 100vh;
+ justify-content: center;
+ align-items: center;
+ color: black;
+}
+
+.login {
+ @include form(0.6);
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ margin: 50px auto;
+ width: 200px;
+ > span {
+ width: 300px;
+ }
+}
+
+.error {
+ color: red;
+ font-size: 1.4rem;
+ white-space: pre-wrap;
+}
+
+.login_text {
+ font-size: 1.6rem;
+ font-weight: 700;
+}
+
+.login_link {
+ display: inline-block;
+ padding: 10px 58px;
+ background-color: $primaryBackgroundButton;
+ border-radius: 0.5rem;
+ color: $secondaryTextColor;
+ font-size: 1.6rem;
+ font-weight: 700;
+ justify-self: center;
+ align-self: center;
+
+ &:hover {
+ background-color: $primaryBackgroundButtonHover;
+ transition: background-color 0.3s ease;
+ color: $secondaryTextColor;
+ }
+
+ &:active {
+ background-color: $primaryBackgroundButtonActive;
+ }
+}
+
+@media (width < 768px) {
+ .login {
+ width: 300px;
+ }
+ .form {
+ > span {
+ width: 200px;
+ }
+ }
+}
diff --git a/src/logic/mainPage/MainPAge.module.scss b/src/logic/mainPage/MainPAge.module.scss
new file mode 100644
index 0000000..42989fd
--- /dev/null
+++ b/src/logic/mainPage/MainPAge.module.scss
@@ -0,0 +1,4 @@
+.main {
+ width: inherit;
+ height: inherit;
+}
diff --git a/src/logic/mainPage/MainPage.tsx b/src/logic/mainPage/MainPage.tsx
new file mode 100644
index 0000000..dec0cc8
--- /dev/null
+++ b/src/logic/mainPage/MainPage.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Layout } from 'components/layout/Layout.tsx';
+import styles from 'src/logic/mainPage/MainPAge.module.scss';
+import style from 'components/layout/Layout.module.scss';
+
+export const Main: React.FC = () => {
+ return (
+
+
+
+ );
+};
diff --git a/src/logic/productPage/Product.tsx b/src/logic/productPage/Product.tsx
new file mode 100644
index 0000000..1463223
--- /dev/null
+++ b/src/logic/productPage/Product.tsx
@@ -0,0 +1,6 @@
+import React from 'react';
+import { CardOne } from 'src/components/card/Card.tsx';
+
+export const Product: React.FC = () => {
+ return ;
+};
diff --git a/src/logic/registrationPage/registration.module.scss b/src/logic/registrationPage/registration.module.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/logic/registrationPage/registrationPage.tsx b/src/logic/registrationPage/registrationPage.tsx
new file mode 100644
index 0000000..e0d7d4e
--- /dev/null
+++ b/src/logic/registrationPage/registrationPage.tsx
@@ -0,0 +1,5 @@
+import { RegistrationForm } from 'src/components/form/registration/RegistrationForm.tsx';
+
+export const RegistrationPage = () => {
+ return ;
+};
diff --git a/src/logic/userProfilePage/UserProfilePage.module.scss b/src/logic/userProfilePage/UserProfilePage.module.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/logic/userProfilePage/UserProfilePage.tsx b/src/logic/userProfilePage/UserProfilePage.tsx
new file mode 100644
index 0000000..73e3a33
--- /dev/null
+++ b/src/logic/userProfilePage/UserProfilePage.tsx
@@ -0,0 +1,5 @@
+import { UserProfileForm } from 'src/components/form/profile/UserProfileForm.tsx';
+
+export const UserProfilePage = () => {
+ return ;
+};
diff --git a/src/model/Customer.ts b/src/model/Customer.ts
new file mode 100644
index 0000000..d6de5b4
--- /dev/null
+++ b/src/model/Customer.ts
@@ -0,0 +1,39 @@
+export interface ICustomerModel {
+ email: string;
+ password: string | undefined;
+ firstName: string | undefined;
+ lastName: string | undefined;
+ dateOfBirth: string | undefined;
+ isShippingDefaultAddress: boolean;
+ isEqualAddress: boolean;
+ streetName: string | undefined;
+ city: string | undefined;
+ postalCode: string | undefined;
+ country: string | undefined;
+ isBillingDefaultAddress: boolean;
+ billingStreet: string | undefined;
+ billingCity: string | undefined;
+ billingCountry: string | undefined;
+ billingPostalCode: string | undefined;
+ version?: number;
+ [key: string]: string | boolean | number | undefined;
+}
+
+export const customerModel: ICustomerModel = {
+ email: '',
+ password: '',
+ firstName: '',
+ lastName: '',
+ dateOfBirth: '',
+ isShippingDefaultAddress: false,
+ isEqualAddress: false,
+ streetName: '',
+ city: '',
+ postalCode: '',
+ country: '',
+ isBillingDefaultAddress: false,
+ billingStreet: '',
+ billingCity: '',
+ billingCountry: '',
+ billingPostalCode: '',
+};
diff --git a/src/public/OpenSans-Regular.ttf b/src/public/OpenSans-Regular.ttf
new file mode 100644
index 0000000..67803bb
Binary files /dev/null and b/src/public/OpenSans-Regular.ttf differ
diff --git a/src/public/error.jpg b/src/public/error.jpg
new file mode 100644
index 0000000..2bc40ea
Binary files /dev/null and b/src/public/error.jpg differ
diff --git a/src/public/favicon.png b/src/public/favicon.png
new file mode 100644
index 0000000..e348575
Binary files /dev/null and b/src/public/favicon.png differ
diff --git a/src/public/hide.png b/src/public/hide.png
new file mode 100644
index 0000000..62dc80a
Binary files /dev/null and b/src/public/hide.png differ
diff --git a/src/public/icons8-close-64.png b/src/public/icons8-close-64.png
new file mode 100644
index 0000000..bb26556
Binary files /dev/null and b/src/public/icons8-close-64.png differ
diff --git a/src/public/login-bg.jpeg b/src/public/login-bg.jpeg
new file mode 100644
index 0000000..d6b2f18
Binary files /dev/null and b/src/public/login-bg.jpeg differ
diff --git a/src/public/main_image2.jpg b/src/public/main_image2.jpg
new file mode 100644
index 0000000..dd9df15
Binary files /dev/null and b/src/public/main_image2.jpg differ
diff --git a/src/public/show.png b/src/public/show.png
new file mode 100644
index 0000000..a659b82
Binary files /dev/null and b/src/public/show.png differ
diff --git a/src/services/DTO/Customer.ts b/src/services/DTO/Customer.ts
new file mode 100644
index 0000000..70c0488
--- /dev/null
+++ b/src/services/DTO/Customer.ts
@@ -0,0 +1,28 @@
+import { Customer } from '@commercetools/platform-sdk';
+import { countryLookup } from 'src/components/country/country.ts';
+import { ICustomerModel } from 'src/model/Customer.ts';
+
+export const mapCustomerToModel = (customer: Customer): ICustomerModel => {
+ const isShippingDefaultAddress = !!customer.defaultShippingAddressId?.toString();
+ const isBillingDefaultAddress = !!customer.defaultBillingAddressId?.toString();
+ const country = countryLookup[customer.addresses[0].country];
+ const billingCountry = countryLookup[customer.addresses[1].country];
+ return {
+ email: customer.email,
+ password: customer.password,
+ firstName: customer.firstName,
+ lastName: customer.lastName,
+ dateOfBirth: customer.dateOfBirth,
+ isShippingDefaultAddress,
+ isEqualAddress: false,
+ streetName: customer.addresses[0].streetName,
+ city: customer.addresses[0]?.city,
+ postalCode: customer.addresses[0]?.postalCode,
+ country,
+ isBillingDefaultAddress,
+ billingStreet: customer.addresses[1]?.streetName,
+ billingCity: customer.addresses[1]?.city,
+ billingCountry,
+ billingPostalCode: customer.addresses[1]?.postalCode,
+ };
+};
diff --git a/src/services/api/BuildClient.ts b/src/services/api/BuildClient.ts
new file mode 100644
index 0000000..bdf003c
--- /dev/null
+++ b/src/services/api/BuildClient.ts
@@ -0,0 +1,73 @@
+import fetch from 'node-fetch';
+import { ClientBuilder } from '@commercetools/sdk-client-v2';
+
+// prettier-ignore
+import type { AuthMiddlewareOptions, HttpMiddlewareOptions } from '@commercetools/sdk-client-v2';
+import { createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
+import { MyTokenCache } from './MyTokenCache.ts';
+import { getCredentials } from '../userData/saveEmailPassword.ts';
+
+const PROJECT_KEY: string = import.meta.env.VITE_CTP_PROJECT_KEY as string;
+const CLIENT_ID: string = import.meta.env.VITE_CTP_CLIENT_ID as string;
+const CLIENT_SECRET: string = import.meta.env.VITE_CTP_CLIENT_SECRET as string;
+const AUTH_URL: string = import.meta.env.VITE_CTP_AUTH_URL as string;
+const API_URL: string = import.meta.env.VITE_CTP_API_URL as string;
+const SCOPESString: string = (import.meta.env.VITE_CTP_SCOPES as string) || '';
+const SCOPES: string[] = SCOPESString.split(' ');
+
+const scopes = SCOPES;
+
+const authMiddlewareOptions: AuthMiddlewareOptions = {
+ host: AUTH_URL,
+ projectKey: PROJECT_KEY,
+ credentials: {
+ clientId: CLIENT_ID,
+ clientSecret: CLIENT_SECRET,
+ },
+ scopes,
+ fetch,
+};
+
+const httpMiddlewareOptions: HttpMiddlewareOptions = {
+ host: API_URL,
+ fetch,
+};
+
+export const getLoginClient = () => {
+ const newTokenCache: MyTokenCache = new MyTokenCache();
+ const PasswordAuthMiddlewareOptions = {
+ host: AUTH_URL,
+ projectKey: PROJECT_KEY,
+ credentials: {
+ clientId: CLIENT_ID,
+ clientSecret: CLIENT_SECRET,
+ user: {
+ username: getCredentials().email,
+ password: getCredentials().password,
+ },
+ },
+ scopes,
+ tokenCache: newTokenCache,
+ fetch,
+ };
+
+ const client = new ClientBuilder()
+ .withProjectKey(PROJECT_KEY)
+ .withPasswordFlow(PasswordAuthMiddlewareOptions)
+ .withHttpMiddleware(httpMiddlewareOptions)
+ // .withLoggerMiddleware() // OFF LOGGER
+ .build();
+
+ return { client, tokenCache: newTokenCache };
+};
+
+export const apiRoot2 = createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({
+ projectKey: PROJECT_KEY,
+});
+
+export const ctpClient = new ClientBuilder()
+ .withProjectKey(PROJECT_KEY)
+ .withAnonymousSessionFlow(authMiddlewareOptions)
+ .withHttpMiddleware(httpMiddlewareOptions)
+ // .withLoggerMiddleware() // OFF LOGGER
+ .build();
diff --git a/src/services/api/BuildClientRegistration.ts b/src/services/api/BuildClientRegistration.ts
new file mode 100644
index 0000000..0e227e3
--- /dev/null
+++ b/src/services/api/BuildClientRegistration.ts
@@ -0,0 +1,113 @@
+import {
+ type Client,
+ ClientBuilder,
+ HttpMiddlewareOptions,
+ AuthMiddlewareOptions,
+} from '@commercetools/sdk-client-v2';
+import fetch from 'node-fetch';
+
+// const VITE_REFRESH_TOKEN: string = import.meta.env.VITE_REFRESH_TOKEN as string;
+export const PROJECT_KEY: string = import.meta.env.VITE_CTP_PROJECT_KEY as string;
+const CLIENT_ID: string = import.meta.env.VITE_CTP_CLIENT_ID as string;
+const CLIENT_SECRET: string = import.meta.env.VITE_CTP_CLIENT_SECRET as string;
+const AUTH_URL: string = import.meta.env.VITE_CTP_AUTH_URL as string;
+const API_URL: string = import.meta.env.VITE_CTP_API_URL as string;
+// const SCOPES: string[] = [import.meta.env.VITE_CTP_SCOPES as string];
+const SCOPESString: string = (import.meta.env.VITE_CTP_SCOPES as string) || '';
+
+const SCOPES: string[] = SCOPESString.split(' ');
+
+const scopes = SCOPES;
+
+// interface TokenCache {
+// myCache: TokenStore;
+// get: () => TokenStore;
+// set: (newCache: TokenStore) => void;
+// }
+
+// interface RefreshAuthMiddlewareOptions {
+// host: string;
+// projectKey: string;
+// credentials: {
+// clientId: string;
+// clientSecret: string;
+// };
+// refreshToken: string;
+// tokenCache?: TokenCache;
+// oauthUri?: string;
+// scopes?: string[];
+// fetch?: unknown;
+// }
+
+// const MytokenCache: TokenCache = {
+// myCache: {
+// token: '',
+// expirationTime: -1,
+// },
+// get() {
+// return this.myCache;
+// },
+// set(newCache) {
+// this.myCache = newCache;
+// },
+// };
+
+const authMiddlewareOptions: AuthMiddlewareOptions = {
+ host: AUTH_URL,
+ projectKey: PROJECT_KEY,
+ credentials: {
+ clientId: CLIENT_ID,
+ clientSecret: CLIENT_SECRET,
+ },
+ scopes,
+ fetch,
+};
+
+// const refreshOptions: RefreshAuthMiddlewareOptions = {
+// host: AUTH_URL,
+// projectKey: PROJECT_KEY,
+// credentials: {
+// clientId: CLIENT_ID,
+// clientSecret: CLIENT_SECRET,
+// },
+// refreshToken: VITE_REFRESH_TOKEN,
+// tokenCache: MytokenCache,
+// scopes: [`manage_project:${PROJECT_KEY}`],
+// fetch,
+// };
+
+// const Httpptions: HttpMiddlewareOptions = {
+// host: API_URL,
+// includeResponseHeaders: true,
+// maskSensitiveHeaderData: true,
+// includeOriginalRequest: false,
+// includeRequestInErrorResponse: false,
+// enableRetry: true,
+// retryConfig: {
+// maxRetries: 3,
+// retryDelay: 200,
+// backoff: false,
+// retryCodes: [503],
+// },
+// fetch,
+// };
+
+const Httpptions: HttpMiddlewareOptions = {
+ host: API_URL,
+ fetch,
+};
+
+// interface QueueMiddlewareOptions {
+// concurrency: number;
+// }
+
+// const queueOptions: QueueMiddlewareOptions = {
+// concurrency: 5,
+// };
+
+export const ctpClientRegistration: Client = new ClientBuilder()
+ .withProjectKey(PROJECT_KEY)
+ .withClientCredentialsFlow(authMiddlewareOptions)
+ .withHttpMiddleware(Httpptions)
+ .withLoggerMiddleware()
+ .build();
diff --git a/src/services/api/MyTokenCache.ts b/src/services/api/MyTokenCache.ts
new file mode 100644
index 0000000..165bc25
--- /dev/null
+++ b/src/services/api/MyTokenCache.ts
@@ -0,0 +1,17 @@
+import { TokenCache, TokenStore } from '@commercetools/sdk-client-v2';
+
+export class MyTokenCache implements TokenCache {
+ myCache: TokenStore = {
+ token: '',
+ expirationTime: 0,
+ refreshToken: '',
+ };
+
+ set(newCache: TokenStore) {
+ this.myCache = newCache;
+ }
+
+ get() {
+ return this.myCache;
+ }
+}
diff --git a/src/services/api/ResetPassword.ts b/src/services/api/ResetPassword.ts
new file mode 100644
index 0000000..2b6bb51
--- /dev/null
+++ b/src/services/api/ResetPassword.ts
@@ -0,0 +1,25 @@
+import { ByProjectKeyRequestBuilder } from '@commercetools/platform-sdk';
+import { IPasswordForm } from 'src/components/form/profile/IPasswordForm.ts';
+
+export const updatePassword = async (
+ api: ByProjectKeyRequestBuilder,
+ formData: IPasswordForm,
+ version: number,
+) => {
+ const accessToken: string = localStorage.getItem('token') ?? '';
+
+ return api
+ .me()
+ .password()
+ .post({
+ body: {
+ version,
+ currentPassword: formData.oldPassword,
+ newPassword: formData.newPassword,
+ },
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ })
+ .execute();
+};
diff --git a/src/services/api/ctpClient.ts b/src/services/api/ctpClient.ts
new file mode 100644
index 0000000..140d929
--- /dev/null
+++ b/src/services/api/ctpClient.ts
@@ -0,0 +1,19 @@
+import { ctpClient } from 'src/services/api/BuildClient.ts';
+import { createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
+
+const PROJECT_KEY: string = import.meta.env.VITE_CTP_PROJECT_KEY as string;
+export const apiRoot = createApiBuilderFromCtpClient(ctpClient).withProjectKey({
+ projectKey: PROJECT_KEY,
+});
+
+// const getProject = async () => {
+// return apiRoot.get().execute();
+// };
+
+// (async () => {
+// await getProject();
+// })();
+
+// export const sdkTest = () => {
+// return true;
+// };
diff --git a/src/services/api/ctpClientRegistration.ts b/src/services/api/ctpClientRegistration.ts
new file mode 100644
index 0000000..6d91cb6
--- /dev/null
+++ b/src/services/api/ctpClientRegistration.ts
@@ -0,0 +1,9 @@
+import { createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
+import { PROJECT_KEY, ctpClientRegistration } from './BuildClientRegistration.ts';
+
+// Create apiRoot from the imported ClientBuilder and include your Project key
+export const apiRootRegistration = createApiBuilderFromCtpClient(
+ ctpClientRegistration,
+).withProjectKey({
+ projectKey: PROJECT_KEY,
+});
diff --git a/src/services/api/filterRequests.ts b/src/services/api/filterRequests.ts
new file mode 100644
index 0000000..22368ff
--- /dev/null
+++ b/src/services/api/filterRequests.ts
@@ -0,0 +1,196 @@
+import { apiRoot } from './ctpClient.ts';
+
+export const fetchAllProducts = async () => {
+ const response = await apiRoot.productProjections().search().get().execute();
+ return response.body.results;
+};
+
+export const fetchCategory = async (category: string) => {
+ let filter = '';
+
+ if (category === 'Decor') {
+ filter = 'categories.id: subtree("59dbad07-067b-46ef-b62e-c6d5eddc2128")';
+ }
+ if (category === 'Wall Decor') {
+ filter = 'categories.id: subtree("043c187b-e2e2-42f4-b7e9-e11e441cbee8")';
+ }
+ if (category === 'X-mas') {
+ filter = 'categories.id: subtree("c7f2242d-4ade-4049-b23c-3aeb238580cc")';
+ }
+ if (category === 'Toys') {
+ filter = 'categories.id: subtree("28b63001-0763-48c2-96e5-fbda8effc91b")';
+ }
+ if (category === 'Food') {
+ filter = 'categories.id: subtree("ed2eba2a-1ebf-4295-ac7f-1080f469fa7d")';
+ }
+ if (category === 'Jar') {
+ filter = 'categories.id: subtree("d32d17fe-3bda-460e-8494-9e50c9ca5a10")';
+ }
+
+ const response = await apiRoot
+ .productProjections()
+ .search()
+ .get({
+ queryArgs: {
+ 'filter.query': [filter],
+ },
+ })
+ .execute();
+ return response.body.results;
+};
+
+export const fetchSearchProducts = async (query: string) => {
+ const response = await apiRoot
+ .productProjections()
+ .search()
+ .get({
+ queryArgs: {
+ 'text.en-US': query,
+ },
+ })
+ .execute();
+ return response.body.results;
+};
+
+export const fetchSortPriceProducts = async (value: string) => {
+ const response = await apiRoot
+ .productProjections()
+ .search()
+ .get({
+ queryArgs: {
+ sort: `price ${value}`,
+ },
+ })
+ .execute();
+ return response.body.results;
+};
+
+export const fetchSortNameProducts = async (value: string) => {
+ const response = await apiRoot
+ .productProjections()
+ .search()
+ .get({
+ queryArgs: {
+ sort: `name.en-US ${value}`,
+ },
+ })
+ .execute();
+ return response.body.results;
+};
+
+export const fetchColorProducts = async (
+ colors: string[],
+ sizesArray: string[],
+ prices: string[],
+) => {
+ const filters = [];
+ const setColor = colors.map((value: string) => `"${value}"`).join(',');
+
+ if (colors.length) {
+ filters.push(`variants.attributes.color-item: ${setColor}`);
+ }
+
+ if (sizesArray.length > 0) {
+ const setSize = sizesArray.map((value: string) => `"${value}"`).join(',');
+ filters.push(`variants.attributes.size-item: ${setSize}`);
+ }
+
+ if (prices.length > 0) {
+ if (prices[0] === 'below 75') {
+ filters.push('variants.price.centAmount: range(0 to 7500)');
+ } else if (prices[0] === '75 to 125') {
+ filters.push('variants.price.centAmount: range(7500 to 12500)');
+ } else if (prices[0] === '125 to 200') {
+ filters.push('variants.price.centAmount: range(12500 to 20000)');
+ } else if (prices[0] === '200 to 250') {
+ filters.push('variants.price.centAmount: range(20000 to 25000)');
+ } else if (prices[0] === 'above 250') {
+ filters.push('variants.price.centAmount: range(25000 to 100000000000000)');
+ }
+ }
+
+ const response = await apiRoot
+ .productProjections()
+ .search()
+ .get({
+ queryArgs: {
+ limit: 30,
+ filter: filters,
+ },
+ })
+ .execute();
+ return response.body.results;
+};
+
+export const fetchSizeProducts = async (
+ sizes: string[],
+ colorsArray: string[],
+ prices: string[],
+) => {
+ const filters = [];
+ const setSize = sizes.map((value: string) => `"${value}"`).join(',');
+
+ if (sizes.length) {
+ filters.push(`variants.attributes.size-item: ${setSize}`);
+ }
+
+ if (colorsArray.length > 0) {
+ const setColor = colorsArray.map((value: string) => `"${value}"`).join(',');
+ filters.push(`variants.attributes.color-item: ${setColor}`);
+ }
+
+ if (prices.length > 0) {
+ if (prices[0] === 'below 75') {
+ filters.push('variants.price.centAmount: range(0 to 7500)');
+ } else if (prices[0] === '75 to 125') {
+ filters.push('variants.price.centAmount: range(7500 to 12500)');
+ } else if (prices[0] === '125 to 200') {
+ filters.push('variants.price.centAmount: range(12500 to 20000)');
+ } else if (prices[0] === '200 to 250') {
+ filters.push('variants.price.centAmount: range(20000 to 25000)');
+ } else if (prices[0] === 'above 250') {
+ filters.push('variants.price.centAmount: range(25000 to 100000000000000)');
+ }
+ }
+
+ const response = await apiRoot
+ .productProjections()
+ .search()
+ .get({
+ queryArgs: {
+ limit: 30,
+ filter: filters,
+ },
+ })
+ .execute();
+ return response.body.results;
+};
+
+export const fetchPriceProducts = async (
+ prices: string,
+ colorsArray: string[],
+ sizesArray: string[],
+) => {
+ const filters = [];
+
+ filters.push(`variants.price.centAmount: range(${prices})`);
+
+ if (colorsArray.length > 0) {
+ const setColor = colorsArray.map((value: string) => `"${value}"`).join(',');
+ filters.push(`variants.attributes.color-item: ${setColor}`);
+ }
+
+ if (sizesArray.length > 0) {
+ const setSize = sizesArray.map((value: string) => `"${value}"`).join(',');
+ filters.push(`variants.attributes.size-item: ${setSize}`);
+ }
+
+ const response = await apiRoot
+ .productProjections()
+ .search()
+ .get({
+ queryArgs: { limit: 30, filter: filters },
+ })
+ .execute();
+ return response.body.results;
+};
diff --git a/src/services/api/loginRequest.ts b/src/services/api/loginRequest.ts
new file mode 100644
index 0000000..1d56397
--- /dev/null
+++ b/src/services/api/loginRequest.ts
@@ -0,0 +1,45 @@
+import { createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
+import { ctpClient, getLoginClient } from './BuildClient.ts';
+import { saveToken } from './saveToken.ts';
+
+export const loginRequest = async (myEmail: string, myPassword: string) => {
+ const PROJECT_KEY: string = import.meta.env.VITE_CTP_PROJECT_KEY as string;
+ const apiRoot = createApiBuilderFromCtpClient(ctpClient).withProjectKey({
+ projectKey: PROJECT_KEY,
+ });
+
+ const loginUser = () => {
+ return apiRoot
+ .me()
+ .login()
+ .post({
+ body: {
+ email: myEmail,
+ password: myPassword,
+ },
+ })
+ .execute();
+ };
+
+ const { client, tokenCache } = getLoginClient();
+
+ const apiRoot2 = createApiBuilderFromCtpClient(client).withProjectKey({
+ projectKey: PROJECT_KEY,
+ });
+
+ const result = loginUser()
+ .then((response) => {
+ const idUser = response.body.customer.id;
+ const newIdUser = idUser.split('-')[0];
+ localStorage.setItem('id', newIdUser);
+ localStorage.setItem('fullID', idUser);
+ })
+ .then(() => {
+ return apiRoot2.carts().get().execute();
+ })
+ .then(() => {
+ saveToken(tokenCache);
+ });
+
+ return result;
+};
diff --git a/src/services/api/registrationCustomer.ts b/src/services/api/registrationCustomer.ts
new file mode 100644
index 0000000..47b30ca
--- /dev/null
+++ b/src/services/api/registrationCustomer.ts
@@ -0,0 +1,19 @@
+import { CustomerDraft, createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
+import { ServerError } from 'src/utils/error/RequestErrors.ts';
+import { ctpClient } from './BuildClient.ts';
+
+export const createCustomer = (newCustomer: CustomerDraft) => {
+ const PROJECT_KEY: string = import.meta.env.VITE_CTP_PROJECT_KEY as string;
+ const apiRoot = createApiBuilderFromCtpClient(ctpClient).withProjectKey({
+ projectKey: PROJECT_KEY,
+ });
+ return apiRoot
+ .customers()
+ .post({
+ body: newCustomer,
+ })
+ .execute()
+ .catch((error: unknown) => {
+ throw new ServerError('Error during customer registration.', error);
+ });
+};
diff --git a/src/services/api/saveToken.ts b/src/services/api/saveToken.ts
new file mode 100644
index 0000000..cba7dd4
--- /dev/null
+++ b/src/services/api/saveToken.ts
@@ -0,0 +1,13 @@
+import { MyTokenCache } from './MyTokenCache.ts';
+
+export const saveToken = (tokenCache: MyTokenCache) => {
+ const { token, refreshToken } = tokenCache.myCache;
+
+ const tokens = {
+ curToken: token,
+ refToken: refreshToken,
+ };
+
+ localStorage.setItem('userTokens', JSON.stringify(tokens));
+ localStorage.setItem('token', tokens.curToken);
+};
diff --git a/src/services/api/updateCustomer.ts b/src/services/api/updateCustomer.ts
new file mode 100644
index 0000000..b3d7cfa
--- /dev/null
+++ b/src/services/api/updateCustomer.ts
@@ -0,0 +1,41 @@
+import { ByProjectKeyRequestBuilder } from '@commercetools/platform-sdk';
+
+export const updateCustomerField = async (
+ apiRoot2: ByProjectKeyRequestBuilder,
+ id: string,
+ customerVersion: number,
+ firstName: string,
+ lastName: string,
+ dateOfBirth: string,
+ email: string,
+) => {
+ const response = await apiRoot2
+ .customers()
+ .withId({ ID: id })
+ .post({
+ body: {
+ version: customerVersion,
+ actions: [
+ {
+ action: 'setFirstName',
+ firstName,
+ },
+ {
+ action: 'setLastName',
+ lastName,
+ },
+ {
+ action: 'setDateOfBirth',
+ dateOfBirth,
+ },
+ {
+ action: 'changeEmail',
+ email,
+ },
+ ],
+ },
+ })
+ .execute();
+
+ return response;
+};
diff --git a/src/services/tools/filterTools.ts b/src/services/tools/filterTools.ts
new file mode 100644
index 0000000..a3b8236
--- /dev/null
+++ b/src/services/tools/filterTools.ts
@@ -0,0 +1,45 @@
+import { fetchAllProducts } from 'src/services/api/filterRequests.ts';
+
+interface Attribute {
+ name: string;
+ value: string[];
+}
+
+interface Product {
+ masterVariant?: {
+ attributes?: Attribute[];
+ };
+}
+
+export const filterTools = async () => {
+ let allColors: string[] = [];
+ let allSizes: string[] = [];
+
+ const productsAll: Product[] = await fetchAllProducts();
+
+ productsAll.forEach((product) => {
+ product.masterVariant?.attributes?.forEach((attribute) => {
+ if (attribute.name === 'color-item') {
+ allColors = allColors.concat(attribute.value);
+ } else if (attribute.name === 'size-item') {
+ allSizes = allSizes.concat(attribute.value);
+ }
+ });
+ });
+
+ const uniqueColors = [...new Set(allColors)];
+ const colors = uniqueColors.map((color) => ({ label: color, value: color }));
+
+ const uniqueSizes = [...new Set(allSizes)];
+ const sizes = uniqueSizes.map((size) => ({ label: size, value: size }));
+
+ const prices = [
+ { label: 'below 75', value: 'below 75' },
+ { label: '75 to 125', value: '75 to 125' },
+ { label: '125 to 200', value: '125 to 200' },
+ { label: '200 to 250', value: '200 to 250' },
+ { label: 'above 250', value: 'above 250' },
+ ];
+
+ return [colors, sizes, prices];
+};
diff --git a/src/services/userData/saveEmailPassword.ts b/src/services/userData/saveEmailPassword.ts
new file mode 100644
index 0000000..75753ce
--- /dev/null
+++ b/src/services/userData/saveEmailPassword.ts
@@ -0,0 +1,26 @@
+let savedEmail = '';
+let savedPassword = 'null';
+
+export const saveCredentials = (email: string, password: string) => {
+ savedEmail = email;
+ savedPassword = password;
+};
+
+export const updateEmail = (email: string) => {
+ savedEmail = email;
+};
+
+export const setPassword = (password: string) => {
+ savedPassword = password;
+};
+
+export const getPassword = (): string => {
+ return savedPassword;
+};
+
+export const getCredentials = () => {
+ return {
+ email: savedEmail,
+ password: savedPassword,
+ };
+};
diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss
new file mode 100644
index 0000000..50dfd43
--- /dev/null
+++ b/src/styles/_mixins.scss
@@ -0,0 +1,56 @@
+@mixin form($opacity) {
+ display: flex;
+ flex-direction: column;
+ width: 474px;
+ background-color: rgba(255, 255, 255, $opacity);
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
+ border-radius: 10px;
+ justify-content: center;
+ align-items: center;
+}
+
+@mixin customer-info-block {
+ padding: 20px;
+ border: 1px solid #ddd;
+ border-radius: 0 5px 5px 5px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+ transition: opacity 0.3s ease-in-out;
+ margin-bottom: 20px;
+ margin-right: 20px;
+}
+
+@mixin media-1200 {
+ @media (max-width: $screen-width-1200) {
+ @content;
+ }
+}
+
+@mixin media-1000 {
+ @media (max-width: $screen-width-1000) {
+ @content;
+ }
+}
+
+@mixin media-800 {
+ @media (max-width: $screen-width-800) {
+ @content;
+ }
+}
+
+@mixin media-600 {
+ @media (max-width: $screen-width-600) {
+ @content;
+ }
+}
+
+@mixin media-500 {
+ @media (max-width: $screen-width-500) {
+ @content;
+ }
+}
+
+@mixin media-400 {
+ @media (max-width: $screen-width-400) {
+ @content;
+ }
+}
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
new file mode 100644
index 0000000..787051d
--- /dev/null
+++ b/src/styles/_variables.scss
@@ -0,0 +1,18 @@
+//color
+$secondaryTextColor: #000000;
+$primaryBackgroundButton: #94b21b;
+$primaryBackgroundButtonHover: #b4d23d;
+$primaryBackgroundButtonActive: #80ee59;
+$secondaryActiveLink: #f8119f;
+$priceSale: #be0a1f;
+
+// container width
+$maxWidthDesktop: 1170px;
+
+//screen width
+$screen-width-1200: 1200px;
+$screen-width-1000: 1000px;
+$screen-width-800: 800px;
+$screen-width-600: 600px;
+$screen-width-500: 500px;
+$screen-width-400: 400px;
diff --git a/src/styles/normalize.css b/src/styles/normalize.css
new file mode 100644
index 0000000..88ab743
--- /dev/null
+++ b/src/styles/normalize.css
@@ -0,0 +1,334 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+ ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+ ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+ margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+ border-bottom: none; /* 1 */
+ text-decoration: underline; /* 2 */
+ text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+ font-family: monospace, monospace; /* 1 */
+ font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+ border-style: none;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 1 */
+ line-height: 1.15; /* 1 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input {
+ /* 1 */
+ overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select {
+ /* 1 */
+ text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type='button'],
+[type='reset'],
+[type='submit'] {
+ -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type='button']::-moz-focus-inner,
+[type='reset']::-moz-focus-inner,
+[type='submit']::-moz-focus-inner {
+ border-style: none;
+ padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type='button']:-moz-focusring,
+[type='reset']:-moz-focusring,
+[type='submit']:-moz-focusring {
+ outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+ padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ * `fieldset` elements in all browsers.
+ */
+
+legend {
+ box-sizing: border-box; /* 1 */
+ color: inherit; /* 2 */
+ display: table; /* 1 */
+ max-width: 100%; /* 1 */
+ padding: 0; /* 3 */
+ white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+ vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type='checkbox'],
+[type='radio'] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type='number']::-webkit-inner-spin-button,
+[type='number']::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type='search'] {
+ -webkit-appearance: textfield; /* 1 */
+ outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type='search']::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button; /* 1 */
+ font: inherit; /* 2 */
+}
+
+/* Interactive
+ ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+ display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+ display: list-item;
+}
+
+/* Misc
+ ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+ display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+ display: none;
+}
diff --git a/src/styles/style.css b/src/styles/style.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/tests/example.test.ts b/src/tests/example.test.ts
new file mode 100644
index 0000000..7610cb1
--- /dev/null
+++ b/src/tests/example.test.ts
@@ -0,0 +1,6 @@
+import { expect, test } from 'vitest';
+
+test('Example test', () => {
+ const result = 1 + 1;
+ expect(result).toEqual(2);
+});
diff --git a/src/utils/CurrencyUtils.ts b/src/utils/CurrencyUtils.ts
new file mode 100644
index 0000000..ec0ae39
--- /dev/null
+++ b/src/utils/CurrencyUtils.ts
@@ -0,0 +1,9 @@
+enum CurrencySymbol {
+ USD = '$',
+ EUR = '€',
+ GBP = '£',
+}
+
+export const getCurrencySymbol = (currencyCode: string | undefined): string | undefined => {
+ return currencyCode ? CurrencySymbol[currencyCode as keyof typeof CurrencySymbol] : undefined;
+};
diff --git a/src/utils/error/RequestErrors.ts b/src/utils/error/RequestErrors.ts
new file mode 100644
index 0000000..1311d09
--- /dev/null
+++ b/src/utils/error/RequestErrors.ts
@@ -0,0 +1,49 @@
+interface CommercetoolsErrorDetail {
+ code: string;
+ message: string;
+}
+
+interface CommercetoolsErrorResponse {
+ statusCode: number;
+ message: string;
+ errors: CommercetoolsErrorDetail[];
+}
+
+export const ErrorType = ['Invalid', 'Error'];
+
+const isCommercetoolsError = (error: unknown): error is { body: CommercetoolsErrorResponse } => {
+ return (
+ typeof error === 'object' &&
+ error !== null &&
+ 'body' in error &&
+ typeof (error as { body: unknown }).body === 'object' &&
+ (error as { body: unknown }).body !== null &&
+ 'errors' in (error as { body: CommercetoolsErrorResponse }).body &&
+ Array.isArray((error as { body: CommercetoolsErrorResponse }).body.errors)
+ );
+};
+
+export const detailedError = (error: unknown) => {
+ const errorsList: string[] = [];
+
+ if (isCommercetoolsError(error)) {
+ error.body.errors.forEach((err: CommercetoolsErrorDetail) => {
+ errorsList.push(`${err.code}: ${err.message}`);
+ });
+ }
+ return errorsList.join('\n');
+};
+
+export class ServerError extends Error {
+ constructor(
+ public message: string,
+ public originalError?: unknown,
+ ) {
+ super(message);
+ this.name = 'ServerError';
+ const detailedErr = detailedError(originalError).split(':').at(1)?.toString();
+ if (detailedErr) {
+ this.message = detailedErr;
+ }
+ }
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..ccbebc7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ "paths": {
+ "src/*": ["./src/*"],
+ "components/*": ["./src/components/*"],
+ "tests/*": ["./src/tests/*"]
+ },
+ "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,
+ "noImplicitAny": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "exclude": ["dist", "node_modules"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..97ede7e
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..05cd038
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react-swc';
+import tsconfigPaths from 'vite-tsconfig-paths';
+
+export default defineConfig({
+ base: './',
+ plugins: [tsconfigPaths(), react()],
+ css: {
+ modules: {
+ localsConvention: 'camelCase',
+ },
+ },
+ resolve: {
+ alias: {
+ 'node-fetch': 'isomorphic-fetch',
+ },
+ },
+});