diff --git a/.eslintrc.cjs b/.eslintrc.cjs index df14a06..1cc07df 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -35,13 +35,14 @@ module.exports = { 'react-hooks', ], rules: { + eqeqeq: 'error', '@typescript-eslint/no-floating-promises': [ 'error', { ignoreIIFE: true, }, ], - eqeqeq: 'error', + 'operator-linebreak': ['error', 'after', { overrides: { '?': 'before', ':': 'before' } }], 'no-console': 'warn', 'no-undef': 'off', @@ -92,6 +93,7 @@ module.exports = { allowConstantExport: true, }, ], + 'react/prop-types': 'off', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', 'jsx-a11y/label-has-associated-control': [ diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b5c68e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Smartphone (please complete the following information):** + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.husky/pre-commit b/.husky/pre-commit index 2d0a86a..a123544 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,3 @@ -npm run format -npm run ci:format +npm run format --staged-files +npm run ci:format --staged-files diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..120777c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +Vadzim. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fc15d31 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +**Welcome!** + +We're excited you're interested in contributing to our project! This guide outlines how you can get involved and make valuable contributions. + +**How to Contribute** + +- **Find an Issue:** + - Check our [Jira board ](https://megaandros.atlassian.net/jira/software/projects/DT/boards/2/backlog?epics=visible) for open issues. + - If you don't find a suitable issue, feel free to discuss new ideas by creating an issue in Jira. +- **Claim the Issue (Optional):** + - Move issue to WIP status to let us know you're working on it. This helps avoid duplicate work. +- **Work on the Issue:** + - Use the Confluence page linked in the issue for detailed specifications and discussions. + - Create branch. You can do it using link within Jira ticket. + - Feel free to ask questions in the comments section of the Jira issue or reach out to us directly. +- **Create a Pull Request:** + - Once you've completed your work, create a pull request (PR) on GitHub. You can do it using link within the Jira ticket. + - Include a clear and concise description of your changes in the PR description. We have a template for PR. + - Reference the related Jira issue in your PR description for easy tracking. Just start all commits from Jira ticket number. Like `git commit -m "DT-100 Add new button"` +- **Get Feedback and Iterate:** + - We will review your PR and provide feedback. + - Address any comments and iterate on your code as needed. + +**Pull Request (PR) Guidelines** + +- **Creation PR:** + - We use template for creation PR, just follow it + - Add screenshots for the smallest and biggest dimestions. See details in README.md +- **Code Style:** + - Follow the existing code style and formatting conventions in the project. + - We used linter or code formatter like [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) to ensure consistency. +- **Testing:** + - Write unit tests for your changes, if applicable. + - Existing tests should continue to pass after your modifications. We use Husky pre-push to run unit tests. +- **Documentation:** + - Update any relevant documentation in Confluence if your changes affect functionality or usage. +- **Clean Commits:** + - Use clear and concise commit messages that describe the changes you made or Comments in PR template. + +**Additional Considerations** + +- **Communication:** + - We value open communication. Feel free to ask questions, share ideas, and discuss challenges in the Jira issue comments or directly reach out to us. Use [discord](https://discord.com/channels/1233036099614543913/1233036100096884747) communication channel +- **Attribution:** + - While we primarily use this project for learning, we appreciate proper attribution if you use or share any significant parts of our code. + +**Thank you for your contributions!** + +We appreciate your willingness to contribute to our project. By following these guidelines, you'll help us keep the project well-organized and maintain high-quality code. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1bf4def --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Vadzim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 0e4635d..97bafbe 100644 --- a/README.md +++ b/README.md @@ -13,33 +13,35 @@ It's a comprehensive online shopping portal that provides an interactive and sea 1. Login and Registration pages 🖥️ 2. Main page 🏠 3. Catalog Product page 📋 -4. Detailed Product page 🔎 +4. Product Detailed page (PDP) 🔎 5. User Profile page 👤 6. Basket page 🛒 7. About Us page 🙋‍♂️🙋‍♀️ +## Layout + +We support layout from 380px till 1440px + ## Technology Stack -0. TypeScript 1. React 2. Vite -3. Loadash -4. AntDesign +3. TypeScript +4. Netlify 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 +11. Git/GitHub ## 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) +3. GitHub (Pull Request, Workflow, Review) ## Design @@ -141,7 +143,7 @@ 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: +to automatically format code using Prettier across all files in the src directory. This ensures consistent code style throughout the project. --cache - check only changed files @@ -155,7 +157,7 @@ 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: +to check code formatting using Prettier across all files in the src directory. This helps identify formatting issues before committing changes. --cache - check only changed files @@ -169,7 +171,7 @@ npm run ci:format ### test -to run the test +to run all tests Example: diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c28cb6e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,24 @@ +## SECURITY.md + +**Reporting Security Vulnerabilities** + +We take the security of our project seriously. If you discover a potential security vulnerability, please report it responsibly by following these guidelines: + +- **Do not disclose the vulnerability publicly.** Public disclosure could be exploited by attackers before a fix is available. +- **Send an email to [rss.security.dreamteam@gmail.com]** with a detailed description of the vulnerability, including steps to reproduce it. +- If possible, provide a proof-of-concept (POC) or exploit code (but do not include any attack scripts). +- We will acknowledge your report within [acknowledgment timeframe, e.g., 48 hours] and work with you to understand the nature and severity of the vulnerability. + +**Supported Versions** + +We only consider security vulnerabilities affecting the following project versions: + +- [List supported versions, e.g., v1.x, v2.0] + +**Security Policy Updates** + +We will update this document as needed to reflect changes in our security practices or the project itself. + +**Disclaimer** + +This project is intended for educational purposes only. While we strive to maintain good security practices, we cannot guarantee the complete absence of vulnerabilities. We encourage you to use this project at your own risk. diff --git a/package.json b/package.json index 7ed5d17..f0e2532 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "main": "./src/main.tsx", "husky": { "hooks": { - "pre-commit": "lint-staged", + "pre-commit": "npm run format --staged-files", "pre-push": "npx validate-branch-name" } }, diff --git a/src/components/address/Address.tsx b/src/components/address/Address.tsx index e2ac5d8..006c46f 100644 --- a/src/components/address/Address.tsx +++ b/src/components/address/Address.tsx @@ -14,7 +14,7 @@ interface AddressProps { postalCode?: string | undefined; }; handleChange: (event: React.ChangeEvent) => void; - handleBoolean?: (value: boolean) => void; + handleBoolean?: (field: string, value: boolean) => void; handleSameAddress?: (value: boolean) => void; errors: { streetName: string | undefined; @@ -50,7 +50,14 @@ export const AddressForm: React.FC = ({ id="shippingCheckbox" label="Set as default address" checked={formData?.isShippingDefaultAddress ?? false} - onChange={handleBoolean} + onChange={() => { + if ( + typeof handleBoolean === 'function' && + formData?.isShippingDefaultAddress !== undefined + ) { + handleBoolean('isShippingDefaultAddress', !formData.isShippingDefaultAddress); + } + }} disabledMode={disabledMode} /> {showIsTheSameAddress && ( diff --git a/src/components/address/BillingAddress.tsx b/src/components/address/BillingAddress.tsx index b89fd3a..6f7ac98 100644 --- a/src/components/address/BillingAddress.tsx +++ b/src/components/address/BillingAddress.tsx @@ -13,7 +13,7 @@ interface BillingAddressProps { billingPostalCode: string | undefined; }; handleChange: (event: React.ChangeEvent) => void; - handleBoolean: (value: boolean) => void; + handleBoolean: (field: string, value: boolean) => void; errors: { billingStreet: string | undefined; billingCity: string | undefined; @@ -40,7 +40,9 @@ export const BillingAddressForm: React.FC = ({ id="billingCheckbox" label="Set as default address" checked={formData.isBillingDefaultAddress} - onChange={handleBoolean} + onChange={() => { + handleBoolean('isBillingDefaultAddress', !formData.isBillingDefaultAddress); + }} disabledMode={disabledMode} /> diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx new file mode 100644 index 0000000..0340832 --- /dev/null +++ b/src/components/button/Button.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +interface ButtonProps { + title: string; + className: string; + disabled?: boolean; + onClick?: (e: React.MouseEvent) => void; +} + +export const Button: React.FC = ({ title, className, disabled, onClick }) => { + return ( + + ); +}; diff --git a/src/components/card/Card.module.scss b/src/components/card/Card.module.scss index 9ff7a35..00d437d 100644 --- a/src/components/card/Card.module.scss +++ b/src/components/card/Card.module.scss @@ -175,3 +175,74 @@ .images_container2 > ul > li > button::before { color: #ffffff; } +.button__remove, +.button__add { + font-size: 1.6rem; + font-weight: 700; + border-radius: 0.5rem; + padding: 5px 10px; + color: $secondaryTextColor; + &:hover { + cursor: pointer; + } +} + +.button__add { + background-color: $primaryBackgroundButton; + &:hover { + background-color: $primaryBackgroundButtonHover; + } + &:active { + background-color: $primaryBackgroundButtonActive; + } + &:disabled { + cursor: auto; + background-color: gray; + } +} + +.button__remove { + background-color: $secondaryBackgroundButton; + &:hover { + background-color: $secondaryBackgroundButtonHover; + } + &:active { + background-color: $secondaryBackgroundButtonActive; + } + &:disabled { + cursor: auto; + background-color: gray; + } +} + +.button__add.disabled, +.button__remove.disabled { + cursor: auto; + background-color: gray; +} + +.buttons { + display: flex; + gap: 20px; + margin-bottom: 20px; + @include media-800 { + width: 40%; + flex-direction: column; + margin-left: auto; + margin-right: auto; + } + @include media-600 { + width: 55%; + } + @include media-400 { + width: 70%; + } +} + +.message__remove { + font-size: 1.2rem; + font-weight: 700; + padding: 5px 10px; + color: $secondaryTextColor; + background-color: $primaryBackgroundButton; +} diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx index 04b175f..9c3911b 100644 --- a/src/components/card/Card.tsx +++ b/src/components/card/Card.tsx @@ -2,14 +2,16 @@ 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'; +import { ByProjectKeyRequestBuilder, ProductCatalogData } from '@commercetools/platform-sdk'; +import { createApiRoot, createLoginApiRoot } from 'src/services/api/BuildClient.ts'; +import { Button } from 'src/components/button/Button.tsx'; +import { addProduct, deleteProductOnProductPage } from 'src/utils/BasketUtils.ts'; interface Image { url: string; @@ -20,12 +22,22 @@ interface IProductData { masterData: ProductCatalogData; } +let apiRoot: ByProjectKeyRequestBuilder; + +function updateApiRoot() { + const isUser = Boolean(localStorage.getItem('userTokens')); + apiRoot = isUser ? createLoginApiRoot() : createApiRoot(); +} + 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 [isButtonDisabled, setIsButtonDisabled] = useState(false); + const [responseStatus, setResponseStatus] = useState(''); + const [showMessage, setShowMessage] = useState(false); const { id } = useParams<{ id: string }>(); const handleImageChange = (newImage: Image, index: number) => { @@ -41,6 +53,18 @@ export const CardOne: React.FC = () => { setModal(false); }; + updateApiRoot(); + + const handleDeleteProduct = async (idProduct: string) => { + await deleteProductOnProductPage(idProduct, (message) => { + setResponseStatus(message); + setShowMessage(true); + setTimeout(() => { + setShowMessage(false); + }, 1000); + }); + }; + const isImages = product?.masterData.staged.masterVariant.images ?? []; const isLength = isImages.length > 1; const isFirstPrice = product?.masterData.current.masterVariant.prices; @@ -54,6 +78,7 @@ export const CardOne: React.FC = () => { const isCurrencyNoDiscount = isFirstPrice ? isFirstPrice[0].value.currencyCode : ''; const currencyCode = getCurrencySymbol(isCurrencyCode) ?? ''; const currencyNoDiscount = getCurrencySymbol(isCurrencyNoDiscount) ?? ''; + const isDisabled = localStorage.getItem('isInBasket'); useEffect(() => { if (typeof id !== 'undefined') { @@ -188,6 +213,47 @@ export const CardOne: React.FC = () => { /> )} +
+
)} diff --git a/src/components/cards/Cards.module.scss b/src/components/cards/Cards.module.scss index ac49ef7..254dffd 100644 --- a/src/components/cards/Cards.module.scss +++ b/src/components/cards/Cards.module.scss @@ -1,11 +1,13 @@ @import 'src/styles/variables'; +@import 'src/styles/mixins'; .cards_container { display: flex; flex-wrap: wrap; - width: 80%; + max-width: 1300px; padding: 20px; - justify-content: space-between; + justify-content: center; + gap: 20px; border-radius: 5px; } @@ -23,6 +25,16 @@ cursor: pointer; transition: 0.3s ease; user-select: none; + @include media-600 { + width: 300px; + height: 450px; + font-size: 0.6rem; + } + @include media-500 { + width: 200px; + height: 550px; + font-size: 0.4rem; + } } .card:hover { @@ -36,7 +48,7 @@ .image_container { display: flex; - width: 90%; + justify-content: center; transition: 0.3s ease; } @@ -49,8 +61,8 @@ .card_info { display: flex; flex-direction: column; - width: 90%; gap: 10px; + padding: 10px; } .image { @@ -65,6 +77,12 @@ .name_thing { color: $secondaryTextColor; margin-bottom: 0px; + @include media-600 { + font-size: 1.8rem; + } + @include media-500 { + font-size: 1.6rem; + } } .description { @@ -76,4 +94,33 @@ .image_container:hover { transform: none; } + .image { + width: 100px; + height: 100px; + } +} + +.button__add { + padding: 5px 10px; + background-color: $primaryBackgroundButton; + border-radius: 0.5rem; + color: $secondaryTextColor; + font-size: 1.6rem; + font-weight: 700; + &:hover { + background-color: $primaryBackgroundButtonHover; + cursor: pointer; + } + &:active { + background-color: $primaryBackgroundButtonActive; + } + &:disabled { + cursor: auto; + background-color: gray; + } +} + +.load { + font-size: 1.2rem; + color: $secondaryTextColor; } diff --git a/src/components/cards/Cards.tsx b/src/components/cards/Cards.tsx index 6b15b1c..ba69582 100644 --- a/src/components/cards/Cards.tsx +++ b/src/components/cards/Cards.tsx @@ -1,16 +1,65 @@ -import React from 'react'; +import React, { useEffect, useState } 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'; +import { ensureBasketAndCheckProduct } from 'src/utils/BasketUtils.ts'; +import { Button } from 'src/components/button/Button.tsx'; +import { fetchAllProducts } from 'src/services/api/filterRequests.ts'; +import { + addProductToCart, + createAnonymousBasket, + getProductsInCart, +} from 'src/services/api/ApiBasket.ts'; +import ImageWithLoader from 'src/components/spinnerImage/ImageWithLoader.tsx'; interface CardProps { products: ProductProjection[]; } export const Card: React.FC = ({ products }) => { + const [isDisabled, setIsDisabled] = useState>({}); + const [isLoading, setIsLoading] = useState>({}); + + const ensureBasketAndCheckProducts = async () => { + const cartId = await createAnonymousBasket(); + const allProducts = await fetchAllProducts(); + const productsInCartResponse = await getProductsInCart(cartId); + const productsInCart = productsInCartResponse.body.lineItems; + + const buttonsState: Record = {}; + + allProducts.forEach((product) => { + const isProductInCart = productsInCart.some( + (cartProduct) => cartProduct.productId === product.id, + ); + buttonsState[product.id] = isProductInCart; + }); + setIsDisabled(buttonsState); + }; + + const addProduct = async (idProduct: string) => { + setIsLoading((prevState) => ({ ...prevState, [idProduct]: true })); + try { + const cartId = await createAnonymousBasket(); + if (cartId && idProduct) { + await addProductToCart(cartId, idProduct); + setIsDisabled((prevState) => ({ ...prevState, [idProduct]: true })); + } + } finally { + setIsLoading((prevState) => ({ ...prevState, [idProduct]: false })); + await ensureBasketAndCheckProducts(); + } + }; + + useEffect(() => { + ensureBasketAndCheckProducts().catch(() => { + 'Button is broken'; + }); + }, []); + return (
{products.map((product) => { @@ -39,11 +88,16 @@ export const Card: React.FC = ({ products }) => { key={product.id} id={product.id} to={`/product/${product.id}`} + onClick={() => { + (async () => { + await ensureBasketAndCheckProduct(product.id); + })(); + }} >
- {product.name['en-US']}
@@ -61,6 +115,20 @@ export const Card: React.FC = ({ products }) => { )}
+ + { + setPromoCode(e.target.value); + }} + placeholder="Enter promo code" + className={style.promoCodeInput} + /> + +
+ {originalTotalPrice - discountedTotalPrice !== 0 && ( +
+

+ Original Total: + {' '} + + {originalTotalPrice} + {` ${cartItems[0]?.price.value.currencyCode}`} + +

+
+ )} +
+

+ Total: + {' '} + {discountedTotalPrice} + {` ${cartItems[0]?.price.value.currencyCode ?? ''}`} +

+
+
+ + + {modalData.message && } + + ); +}; diff --git a/src/components/form/form.tsx b/src/components/form/form.tsx index 3207d8d..726da40 100644 --- a/src/components/form/form.tsx +++ b/src/components/form/form.tsx @@ -100,16 +100,19 @@ export const Form = () => { EMAIL { setEmail(e.target.value); }} required + autoComplete="email" /> {errorEmail} PASSWORD { @@ -122,6 +125,7 @@ export const Form = () => { Show Password { diff --git a/src/components/form/profile/AddressProfileForm.tsx b/src/components/form/profile/AddressProfileForm.tsx index 17c6f84..144ff6f 100644 --- a/src/components/form/profile/AddressProfileForm.tsx +++ b/src/components/form/profile/AddressProfileForm.tsx @@ -1,7 +1,6 @@ -import { Address, createApiBuilderFromCtpClient } from '@commercetools/platform-sdk'; +import { Address } 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 { createLoginApiRoot } from 'src/services/api/BuildClient.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'; @@ -17,9 +16,7 @@ interface AddressProfileProps { } export const AddressProfileForm: React.FC = ({ userProfileFormData }) => { - const apiRoot = createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({ - projectKey: PROJECT_KEY, - }); + const loginApiRoot = createLoginApiRoot(); const [countryNewAddress] = useState(Country.Underfined); const [countryBilling] = useState(Country.Underfined); @@ -64,12 +61,11 @@ export const AddressProfileForm: React.FC = ({ userProfileF }; useEffect(() => { - const apiRoot2 = createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({ - projectKey: PROJECT_KEY, - }); + const loginApiRoot2 = createLoginApiRoot(); + const fetchAddresses = async (): Promise => { try { - const response = await apiRoot2.customers().withId({ ID: id }).get().execute(); + const response = await loginApiRoot2.customers().withId({ ID: id }).get().execute(); setAddresses(response.body.addresses); setFormData((prevFormData) => ({ ...prevFormData, @@ -115,7 +111,7 @@ export const AddressProfileForm: React.FC = ({ userProfileF const fetchLatestVersion = async (): Promise => { try { - const response = await apiRoot.customers().withId({ ID: id }).get().execute(); + const response = await loginApiRoot.customers().withId({ ID: id }).get().execute(); return response.body.version; } catch (error) { proceedExceptions(error, 'Fetching latest version'); @@ -127,7 +123,7 @@ export const AddressProfileForm: React.FC = ({ userProfileF const latestVersion = await fetchLatestVersion(); if (latestVersion !== null) { try { - const response = await apiRoot + const response = await loginApiRoot .customers() .withId({ ID: id }) .post({ @@ -166,7 +162,7 @@ export const AddressProfileForm: React.FC = ({ userProfileF const latestVersion = await fetchLatestVersion(); if (latestVersion !== null) { try { - const response = await apiRoot + const response = await loginApiRoot .customers() .withId({ ID: id }) .post({ @@ -207,7 +203,7 @@ export const AddressProfileForm: React.FC = ({ userProfileF const latestVersion = await fetchLatestVersion(); if (latestVersion !== null) { try { - const response = await apiRoot + const response = await loginApiRoot .customers() .withId({ ID: id }) .post({ diff --git a/src/components/form/profile/BasicUserDataProfile.tsx b/src/components/form/profile/BasicUserDataProfile.tsx index 0b5e7f4..0c551ea 100644 --- a/src/components/form/profile/BasicUserDataProfile.tsx +++ b/src/components/form/profile/BasicUserDataProfile.tsx @@ -1,10 +1,8 @@ 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 { createLoginApiRoot } from 'src/services/api/BuildClient.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'; @@ -21,10 +19,9 @@ interface BasicUserDataProfileProps { export const BasicUserDataProfile: React.FC = ({ userProfileFormData, }) => { - const apiRoot2 = createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({ - projectKey: PROJECT_KEY, - }); - const [api, setAPI] = useState(apiRoot2); + const loginApiRoot = createLoginApiRoot(); + + const [api, setAPI] = useState(loginApiRoot); const [id] = useState(localStorage.getItem('fullID') ?? ''); const [isDisabledUserInfo, setEditUserInfo] = useState(true); @@ -123,11 +120,7 @@ export const BasicUserDataProfile: React.FC = ({ updateEmail(formData.email); - setAPI( - createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({ - projectKey: PROJECT_KEY, - }), - ); + setAPI(createLoginApiRoot()); setFormData(customerData); setModalData({ diff --git a/src/components/form/profile/ChangePasswordForm.tsx b/src/components/form/profile/ChangePasswordForm.tsx index f2e0f7c..0ebcd25 100644 --- a/src/components/form/profile/ChangePasswordForm.tsx +++ b/src/components/form/profile/ChangePasswordForm.tsx @@ -5,9 +5,7 @@ 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 { createLoginApiRoot } from 'src/services/api/BuildClient.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'; @@ -18,10 +16,8 @@ interface ChangePasswordFormProps { } export const ChangePasswordForm: React.FC = ({ version }) => { - const apiRoot2 = createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({ - projectKey: PROJECT_KEY, - }); - const [api, setAPI] = useState(apiRoot2); + const loginApiRoot = createLoginApiRoot(); + const [api, setAPI] = useState(loginApiRoot); const [isDisabledPassword, setEditPassword] = useState(true); const [errors, setErrors] = useState(passwordForm); @@ -107,11 +103,7 @@ export const ChangePasswordForm: React.FC = ({ version setModalData({ status: 'Success', message: 'Password updated successfully.' }); setEditPassword(true); - setAPI( - createApiBuilderFromCtpClient(getLoginClient().client).withProjectKey({ - projectKey: PROJECT_KEY, - }), - ); + setAPI(createLoginApiRoot()); }) .catch((error: unknown) => { if (error instanceof ServerError) { diff --git a/src/components/form/profile/UseModalEffect.ts b/src/components/form/profile/UseModalEffect.ts index 3bb3637..9212894 100644 --- a/src/components/form/profile/UseModalEffect.ts +++ b/src/components/form/profile/UseModalEffect.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -const useModalEffect = ( +export const useModalEffect = ( modalData: { status: string; message: string }, setModalData: React.Dispatch>, ) => { diff --git a/src/components/form/profile/UserProfileForm.tsx b/src/components/form/profile/UserProfileForm.tsx index 02a1b7c..ff441bd 100644 --- a/src/components/form/profile/UserProfileForm.tsx +++ b/src/components/form/profile/UserProfileForm.tsx @@ -1,26 +1,18 @@ 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 { createLoginApiRoot } 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 loginApiRoot = createLoginApiRoot(); + const [api] = useState(loginApiRoot); const [activeTab, setActiveTab] = useState('basicInfo'); const [id] = useState(localStorage.getItem('fullID') ?? ''); diff --git a/src/components/form/registration/RegistrationForm.tsx b/src/components/form/registration/RegistrationForm.tsx index ba6050a..5d28175 100644 --- a/src/components/form/registration/RegistrationForm.tsx +++ b/src/components/form/registration/RegistrationForm.tsx @@ -7,17 +7,14 @@ 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'; @@ -42,13 +39,9 @@ export const RegistrationForm: React.FC = () => { 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(); @@ -56,16 +49,10 @@ export const RegistrationForm: React.FC = () => { countryShipping = Country[formData.country as keyof typeof Country]; countryBilling = Country[formData.billingCountry as keyof typeof Country]; - const handleDefaultAddress = (checked: boolean) => { + const handleAddress = (field: string, checked: boolean) => { setFormData({ ...formData, - isShippingDefaultAddress: checked, - }); - }; - const handleBillingAddress = (checked: boolean) => { - setFormData({ - ...formData, - isBillingDefaultAddress: checked, + [field]: checked, }); }; @@ -167,9 +154,6 @@ export const RegistrationForm: React.FC = () => { 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(); }; @@ -205,15 +189,10 @@ export const RegistrationForm: React.FC = () => { 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); - } + if (body.customer.email && formData.password) { + saveCredentials(formData.email, formData.password); + setCurrentUser({ ...body.customer }); + await loginRequest(formData.email, formData.password); setTimeout(() => { navigation('/'); @@ -224,65 +203,8 @@ export const RegistrationForm: React.FC = () => { .catch((error: unknown) => { if (error instanceof ServerError) { setModalData({ status: 'Error', message: error.message }); - } + } else setModalData({ status: 'Error', message: 'Uknown error during registration' }); }); - - 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 - } - }); - } } }; @@ -293,14 +215,14 @@ export const RegistrationForm: React.FC = () => { { const navigation = useNavigate(); const [activeLink, setActiveLink] = useState(location); const [isLoggedIn, setIsLoggedIn] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + // const countProducts = 0; useEffect(() => { setActiveLink(location); const user = localStorage.getItem('userTokens'); @@ -48,6 +50,18 @@ export const Header: React.FC = () => { setIsLoggedIn(false); Form(); }; + + const toggleMenu = () => { + setIsMenuOpen((prevState) => !prevState); + document.body.classList.add('open'); + document.body.style.overflow = ''; + }; + + const closeMenu = () => { + setIsMenuOpen(false); + document.body.classList.remove('open'); + }; + const isProductPage = location.startsWith('/product/'); const is404Page = !isProductPage && @@ -55,7 +69,9 @@ export const Header: React.FC = () => { location !== '/login' && location !== '/register' && location !== '/catalog' && - location !== '/profile'; + location !== '/profile' && + location !== '/about_us' && + location !== '/basket'; const isHeaderInactive = location === '/'; const isToken = localStorage.getItem('userTokens'); return ( @@ -66,12 +82,34 @@ export const Header: React.FC = () => { title="Cozy House" className={`${styles.logo} ${isHeaderInactive ? styles.inactive : ''}`} /> -