├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.ico ├── logo192.png ├── logo512.png └── robots.txt ├── src ├── App.jsx ├── assets │ └── images │ │ ├── active-cart-overlay.png │ │ ├── active-hamburger.png │ │ ├── add-to-cart.png │ │ ├── arrow-icon.png │ │ ├── cart-hero.webp │ │ ├── cart-icon.png │ │ ├── inactive-hamburger.png │ │ ├── landing-images │ │ ├── gridFive.webp │ │ ├── gridFour.webp │ │ ├── gridOne.webp │ │ ├── gridThree.webp │ │ ├── gridTwo.webp │ │ └── hero-cover.webp │ │ ├── product-listing-images │ │ ├── category-all-hero.webp │ │ ├── category-blouses-hero.webp │ │ ├── category-dresses-hero.webp │ │ ├── category-jeans-hero.webp │ │ └── category-shoes-hero.webp │ │ └── project-preview.webp ├── components │ ├── AddToCartButton.jsx │ ├── ChangeCartItemQuantity.jsx │ ├── SuccessMessage.jsx │ ├── attributes │ │ ├── Attributes.jsx │ │ ├── SelectedAttributes.jsx │ │ └── attributes.css │ ├── cart-overlay │ │ ├── CartIcon.jsx │ │ ├── CartOverlay.jsx │ │ ├── CartOverlayItem.jsx │ │ └── cart-overlay.css │ ├── currency-overlay │ │ ├── CurrencyIcon.jsx │ │ ├── CurrencyOverlay.jsx │ │ └── currency-overlay.css │ └── header │ │ ├── CategoryMenu.jsx │ │ ├── Header.jsx │ │ └── header.css ├── core-ui │ ├── hovers.css │ ├── responsive.css │ └── styles.css ├── data │ └── all-products.js ├── database │ └── firebase.js ├── helpers │ └── ResetLocation.jsx ├── index.jsx └── routes │ ├── all-products │ ├── AllProducts.jsx │ ├── Product.jsx │ ├── QuickAddToCart.jsx │ └── all-products.css │ ├── cart │ ├── Cart.jsx │ ├── CartSingleItem.jsx │ ├── CartTotals.jsx │ └── cart.css │ ├── checkout │ ├── Checkout.jsx │ ├── CheckoutSingleItem.jsx │ └── checkout.css │ ├── landing │ ├── Landing.jsx │ └── landing.css │ ├── not-found │ ├── NotFound.jsx │ └── not-found.css │ ├── order │ ├── Order.jsx │ └── order.css │ └── single-product │ ├── ProductShowcase.jsx │ ├── ProductTitles.jsx │ ├── SingleProduct.jsx │ └── single-product.css ├── vercel.json └── vite.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.mp4 filter=lfs diff=lfs merge=lfs -text 2 | *.psd filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .env 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | 16 | /dist 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ekaterine (Catherine) Mitagvaria 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Shopping Time](https://raw.githubusercontent.com/catherineisonline/shopping-time/main/src/assets/images/project-preview.webp) 2 | 3 | # [Shopping Time](https://shopping-time.vercel.app/) 4 | Shopping Time is an e-commerce website that has at least 150 women's clothing products with different sizes and color choices. You can choose various attributes like size and color. There is a cart where you can see added items and edit, add, or remove them. You can also set a currency of your choice. 5 | 6 | ## Functionality Overview 7 | Below is a comprehensive overview of the functionalities that the website offers: 8 | 9 | ### Managing Your Cart 10 | - Add and Remove Products: The website allows you to easily add products to your shopping cart with just a few clicks. You can also remove items from your cart when you change your mind or no longer wish to purchase them. 11 | 12 | - Adjust Quantity: In addition to adding and removing items, you have the flexibility to change the quantity of products in your cart. Whether you want one more of your favorite item or need to reduce the quantity, it's a breeze. 13 | 14 | - Cart Overlay: There is a convenient cart overlay that displays a summary of the items currently in your cart. This allows you to keep track of your selected items without navigating away from your shopping experience. 15 | 16 | - Product Attributes: When reviewing your cart or cart overlay, you'll find detailed information about each product, including selected size and other relevant attributes. This ensures you have a clear understanding of your choices before proceeding to checkout. 17 | 18 | ### Streamlined Shopping 19 | - Category Filtering: The website makes it easy to find products within your preferred categories. You can filter products by various categories, making it simple to locate exactly what you're looking for. 20 | 21 | - Category and Product Page Shopping: Whether you prefer browsing by category or exploring individual product pages, you can add products to your cart from both locations. The website offers a seamless shopping experience to cater to your preferences. 22 | 23 | - Attribute Selection: To maintain accuracy and prevent errors, you won't be able to add products to your cart until you've selected necessary attributes like size or color. This ensures that the items you receive are exactly what you expect. 24 | 25 | ### Flexible Currency Options 26 | - Currency Selection: The customers come from diverse locations around the world. That's why the website offers the flexibility to change the store currency to various options such as EUR, GBP, AUD, JPY, and more. Shop in the currency that suits you best. 27 | 28 | ### Secure and User-Friendly Checkout 29 | - Multi-Step Checkout: The website features a multi-step checkout process to guide you through the purchase smoothly and securely. Each step is designed with user experience in mind. 30 | 31 | - Form Validations: To prevent errors and ensure accurate order information, the checkout page includes form validations. This guarantees that the necessary details are correctly entered, helping to streamline shopping experience. 32 | 33 | ## Goals I achieved 34 | - Practice React Class components and then refactor to hooks 35 | - Build logic to choose attributes and add items to the cart 36 | - Add form validations 37 | - Use Firebase to save data externally 38 | 39 | 40 | ## Getting Started 41 | 42 | To get started you need to: 43 | 44 | 1. Clone the project 45 | 2. pnpm install 46 | 3. Install listed dependencies 47 | 4. Use available scripts, like pnpm start 48 | 49 | ## Dependencies | APIs | Services 50 | 51 | - [React Router](https://www.npmjs.com/package/react-router-dom) 52 | - [React Slider](https://www.npmjs.com/package/react-simple-image-slider) 53 | - [uuid](https://www.npmjs.com/package/uuid) 54 | - [Firestore](https://firebase.google.com/docs/firestore) 55 | 56 | ## Contribution 57 | 58 | This project serves as a personal portfolio website to show off my personal skills. I do not accept any contributions like pull requests to this project however if you have any suggestions or ideas feel free to contact me or submit an idea in the [discussions](https://github.com/catherineisonline/shopping-time/discussions). Otherwise, you are free to fork this project and change it for your own purposes. 59 | 60 | ## License 61 | This project is released under the MIT [LICENSE](https://github.com/catherineisonline/shopping-time/blob/main/LICENSE). You can find the specific terms and conditions outlined in the LICENSE file. This means you're free to utilize, modify, and distribute the project according to the terms of the MIT License. 62 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 24 | 25 | 26 | 28 | 29 | 31 | 32 | 33 | Shopping Time 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopping-time", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "firebase": "^11.6.0", 7 | "react": "^19.1.0", 8 | "react-dom": "^19.1.0", 9 | "react-router-dom": "^7.5.0", 10 | "react-simple-image-slider": "^2.4.1", 11 | "uuid": "^11.1.0" 12 | }, 13 | "devDependencies": { 14 | "@vitejs/plugin-react": "^4.3.4", 15 | "npm-check": "^6.0.1", 16 | "vite": "^6.2.6" 17 | }, 18 | "scripts": { 19 | "dev": "vite", 20 | "start": "vite", 21 | "build": "vite build", 22 | "preview": "pnpm serve -s dist", 23 | "test": "vite test", 24 | "server": "node src/database/index.mjs", 25 | "check-deps": "npm-check" 26 | }, 27 | "pnpm": { 28 | "ignoredBuiltDependencies": [ 29 | "esbuild", 30 | "protobufjs" 31 | ] 32 | } 33 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from "react"; 2 | import { Route, Routes, BrowserRouter } from "react-router-dom"; 3 | import Header from "./components/header/Header.jsx"; 4 | import AllProducts from "./routes/all-products/AllProducts.jsx"; 5 | import SingleProduct from "./routes/single-product/SingleProduct.jsx"; 6 | import Cart from "./routes/cart/Cart.jsx" 7 | import Landing from "./routes/landing/Landing.jsx"; 8 | import Checkout from "./routes/checkout/Checkout.jsx"; 9 | import NotFound from "./routes/not-found/NotFound.jsx"; 10 | import Order from "./routes/order/Order.jsx"; 11 | import products_database from "./database/firebase.js" 12 | import { collection, getDocs } from 'firebase/firestore/lite'; 13 | 14 | const App = () => { 15 | const [activeCategory, setActiveCategory] = useState(''); 16 | const [allCurrencies, setAllCurrencies] = useState([]); 17 | const [selectedCurrency, setSelectedCurrency] = useState('$'); 18 | const [allProducts, setAllProducts] = useState([]); 19 | const [cartItems, setCartItems] = useState([]); 20 | const [totalPayment, setTotalPayment] = useState(0); 21 | const [taxes, setTaxes] = useState(0); 22 | const [productsQuantity, setProductsQuantity] = useState(0); 23 | const [orderFormValue, setOrderFormValue] = useState({}); 24 | const [cachedProducts, setCachedProducts] = useState([]); 25 | const [cachedCurrencies, setCachedCurrencies] = useState([]); 26 | const [isLoading, setIsLoading] = useState(false); 27 | 28 | const GetProducts = useCallback(async (targetcategory) => { 29 | setIsLoading(true); 30 | if (cachedProducts.length === 0) { 31 | try { 32 | const products = await retrieveProducts(products_database); 33 | setCachedProducts(products); 34 | if (targetcategory === 'all') { 35 | setAllProducts(products); 36 | } else { 37 | const targetProducts = products.filter(item => 38 | Object.values(item).includes(targetcategory) 39 | ); 40 | setAllProducts(targetProducts); 41 | } 42 | } catch (error) { 43 | console.error('Error fetching products:', error); 44 | } 45 | } else { 46 | // Use cached products 47 | if (targetcategory === 'all') { 48 | setAllProducts(cachedProducts); 49 | } else { 50 | const targetProducts = cachedProducts.filter(item => 51 | Object.values(item).includes(targetcategory) 52 | ); 53 | setAllProducts(targetProducts); 54 | } 55 | } 56 | setIsLoading(false) 57 | }, [cachedProducts]); 58 | 59 | const retrieveProducts = async (db) => { 60 | const all_products_col = collection(db, 'products'); 61 | const productsSnapshot = await getDocs(all_products_col); 62 | const all_products = productsSnapshot.docs.map(doc => doc.data()); 63 | return all_products; 64 | } 65 | 66 | useEffect(() => { 67 | }, [activeCategory, GetProducts]); 68 | 69 | 70 | 71 | const clearCart = () => { 72 | setCartItems([]); 73 | setProductsQuantity(0); 74 | localStorage.removeItem('cartItems'); 75 | localStorage.removeItem('productsQuantity'); 76 | } 77 | 78 | const changeCategory = (newCategory) => { 79 | 80 | setActiveCategory(newCategory); 81 | GetProducts(newCategory); 82 | localStorage.setItem('activeCategory', JSON.stringify(newCategory)); 83 | }; 84 | 85 | useEffect(() => { 86 | const storedActiveCategory = JSON.parse(localStorage.getItem('activeCategory')); 87 | if (storedActiveCategory) { 88 | setActiveCategory(storedActiveCategory); 89 | GetProducts(storedActiveCategory); 90 | } 91 | }, [GetProducts]); 92 | 93 | useEffect(() => { 94 | const storedSelectedCurrency = JSON.parse(localStorage.getItem('selectedCurrency')); 95 | if (storedSelectedCurrency) { 96 | setSelectedCurrency(storedSelectedCurrency); 97 | } 98 | }, []); 99 | 100 | const changeCurrency = (newSelectedCurrency) => { 101 | setSelectedCurrency(newSelectedCurrency); 102 | localStorage.setItem('selectedCurrency', JSON.stringify(newSelectedCurrency)); 103 | }; 104 | 105 | const getCurrencies = useCallback(async () => { 106 | if (cachedCurrencies.length === 0) { 107 | try { 108 | const all_currencies = await retrieveCurrencies(products_database); 109 | setCachedCurrencies(all_currencies); 110 | 111 | } catch (error) { 112 | console.error('Error fetching currencies:', error); 113 | } 114 | } else { 115 | setAllCurrencies(cachedCurrencies); 116 | } 117 | 118 | }, [cachedCurrencies]); 119 | async function retrieveCurrencies(db) { 120 | try { 121 | const all_currencies_col = collection(db, 'currencies'); 122 | const currenciesSnapshot = await getDocs(all_currencies_col); 123 | const all_currencies = currenciesSnapshot.docs.map(doc => doc.data()); 124 | return all_currencies; 125 | } catch (error) { 126 | console.error('Error retrieving currencies from Firebase:', error); 127 | return []; 128 | } 129 | } 130 | 131 | useEffect(() => { 132 | getCurrencies(); 133 | }, [getCurrencies]); 134 | 135 | const MatchingAttributes = (userSelectedAttributes, targetProduct) => { 136 | const attributesMatch = (groupOne, groupTwo) => { 137 | return Object.values(groupOne)[1] === Object.values(groupTwo)[1]; 138 | }; 139 | 140 | let truthyValuesCounter = 0; 141 | let i = 0; 142 | while (i < userSelectedAttributes.length) { 143 | if ( 144 | attributesMatch( 145 | userSelectedAttributes[i], 146 | targetProduct?.userSelectedAttributes[i] 147 | ) 148 | ) { 149 | truthyValuesCounter += 1; 150 | } 151 | i += 1; 152 | } 153 | 154 | return truthyValuesCounter === userSelectedAttributes?.length; 155 | }; 156 | const updateCartQuantity = (actionToPerfrom, productAlreadyInCart, userSelectedAttributes) => { 157 | const repeatableProduct = CheckRepeatableProducts( 158 | cartItems, 159 | productAlreadyInCart, 160 | userSelectedAttributes 161 | ); 162 | const indexOfRepeatableProduct = cartItems.indexOf(repeatableProduct); 163 | const currentProductList = [...cartItems]; 164 | if (actionToPerfrom === 'addProduct') { 165 | currentProductList[indexOfRepeatableProduct].quantity += 1; 166 | } else { 167 | currentProductList[indexOfRepeatableProduct].quantity -= 1; 168 | } 169 | 170 | return currentProductList; 171 | }; 172 | const CheckRepeatableProducts = (cartItems, targetProduct, userSelectedAttributes) => { 173 | let item; 174 | const productsById = cartItems.filter( 175 | (item) => item.id === targetProduct.id 176 | ); 177 | productsById.forEach((targetProduct) => { 178 | if (MatchingAttributes(userSelectedAttributes, targetProduct)) { 179 | item = targetProduct; 180 | } 181 | }); 182 | return item; 183 | }; 184 | 185 | 186 | const handleAddProduct = (targetProduct, userSelectedAttributes = null) => { 187 | let updatedProductList; 188 | const productAlreadyInCart = CheckRepeatableProducts( 189 | cartItems, 190 | targetProduct, 191 | userSelectedAttributes 192 | ); 193 | 194 | if (productAlreadyInCart) { 195 | updatedProductList = updateCartQuantity( 196 | 'addProduct', 197 | productAlreadyInCart, 198 | userSelectedAttributes 199 | ); 200 | } else { 201 | let modifiedProduct = JSON.parse(JSON.stringify(targetProduct)); 202 | let clone; 203 | 204 | for (let i = 0; i < targetProduct?.attributes?.length; i++) { 205 | for (let j = 0; j < targetProduct?.attributes[i]?.items?.length; j++) { 206 | if ( 207 | targetProduct.attributes[i].items[j].value === 208 | userSelectedAttributes[i].value 209 | ) { 210 | clone = { 211 | ...targetProduct.attributes[i].items[j], 212 | }; 213 | clone.isSelected = true; 214 | 215 | modifiedProduct.attributes[i].items[j].isSelected = true; 216 | 217 | modifiedProduct.attributes[i].items[j] = { 218 | ...clone, 219 | }; 220 | } 221 | } 222 | } 223 | updatedProductList = [ 224 | ...cartItems, 225 | { 226 | ...modifiedProduct, 227 | userSelectedAttributes, 228 | quantity: 1, 229 | }, 230 | ]; 231 | } 232 | 233 | // Create unique id 234 | updatedProductList.map((updatedProduct) => { 235 | const firstValue = Object.values( 236 | updatedProduct.userSelectedAttributes[0] || [] 237 | ); 238 | const secondValue = Object.values( 239 | updatedProduct.userSelectedAttributes[1] || [] 240 | ); 241 | const thirdValue = Object.values( 242 | updatedProduct.userSelectedAttributes[2] || [] 243 | ); 244 | 245 | const productId = updatedProduct.id; 246 | return (updatedProduct.uniqueId = `${productId}-${firstValue}-${secondValue}-${thirdValue}`); 247 | }); 248 | // Update cart items 249 | setCartItems(updatedProductList); 250 | localStorage.setItem('cartItems', JSON.stringify(updatedProductList)); 251 | 252 | // Update cart quantity 253 | if (updatedProductList.length <= 1) { 254 | updatedProductList.forEach((item) => { 255 | localStorage.setItem('productsQuantity', JSON.stringify(item.quantity)); 256 | setProductsQuantity(item.quantity); 257 | }); 258 | } else { 259 | const productListArray = updatedProductList.map((item) => item.quantity); 260 | const sum = productListArray.reduce((a, b) => a + b, 0); 261 | setProductsQuantity(sum); 262 | localStorage.setItem('productsQuantity', JSON.stringify(sum)); 263 | } 264 | }; 265 | useEffect(() => { 266 | if (localStorage.getItem('cartItems') !== null) { 267 | const jsonCartItems = localStorage.getItem('cartItems'); 268 | const cartItems = JSON.parse(jsonCartItems); 269 | setCartItems(cartItems); 270 | } if (localStorage.getItem('productsQuantity') !== null) { 271 | const jsonProductsQuantity = localStorage.getItem('productsQuantity'); 272 | const productsQuantity = JSON.parse(jsonProductsQuantity); 273 | setProductsQuantity(productsQuantity); 274 | } 275 | }, []) 276 | 277 | 278 | const alertMessageMain = () => { 279 | const alertMessage = document.querySelector('.success-alert'); 280 | alertMessage.classList.add('visible'); 281 | setTimeout(() => { 282 | alertMessage.classList.remove('visible'); 283 | }, 1000); 284 | }; 285 | const handleRemoveProduct = (targetProduct, userSelectedAttributes) => { 286 | let updatedProductList; 287 | const repeatableProduct = CheckRepeatableProducts( 288 | cartItems, 289 | targetProduct, 290 | userSelectedAttributes 291 | ); 292 | if (repeatableProduct.quantity > 1) { 293 | updatedProductList = updateCartQuantity( 294 | 'removeProduct', 295 | repeatableProduct, 296 | userSelectedAttributes 297 | ); 298 | } else { 299 | const products = [...cartItems]; 300 | const indexOfProduct = products.indexOf(repeatableProduct); 301 | products.splice(indexOfProduct, 1); 302 | updatedProductList = products; 303 | } 304 | 305 | // Update cart items 306 | setCartItems(updatedProductList); 307 | localStorage.setItem('cartItems', JSON.stringify(updatedProductList)); 308 | 309 | // Update cart quantity 310 | if (updatedProductList.length <= 1) { 311 | updatedProductList.forEach((item) => { 312 | localStorage.setItem('productsQuantity', JSON.stringify(item.quantity)); 313 | setProductsQuantity(item.quantity); 314 | }); 315 | } else { 316 | const productListArray = updatedProductList.map((item) => item.quantity); 317 | const sum = productListArray.reduce((a, b) => a + b); 318 | setProductsQuantity(sum); 319 | localStorage.setItem('productsQuantity', JSON.stringify(sum)); 320 | } 321 | if (updatedProductList.length === 0) { 322 | setProductsQuantity(0); 323 | localStorage.setItem('productsQuantity', JSON.stringify(0)); 324 | } 325 | }; 326 | const getPrice = (prices, currency) => { 327 | return prices.filter((price) => price.currency.symbol === currency)[0]; 328 | }; 329 | 330 | 331 | // get total price of cart items 332 | const getTotalPrice = useCallback( 333 | (selectedCurrency, cartItems) => { 334 | let totalPayment = 0; 335 | for (const item of cartItems) { 336 | const correctPrice = getPrice(item.prices, selectedCurrency); 337 | 338 | totalPayment = totalPayment + correctPrice.amount * item.quantity; 339 | } 340 | 341 | totalPayment = parseFloat(totalPayment.toFixed(2)); 342 | 343 | setTotalPayment(totalPayment); 344 | setTaxes(((totalPayment * 21) / 100).toFixed(2)); 345 | }, [setTotalPayment, setTaxes]); 346 | 347 | useEffect(() => { 348 | getTotalPrice(selectedCurrency, cartItems); 349 | }, [cartItems, selectedCurrency, getTotalPrice]); 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | return ( 358 | 359 |
374 | 375 | 376 | } 379 | /> 380 | 391 | } 392 | /> 393 | 402 | } 403 | /> 404 | 417 | } 418 | /> 419 | 0 ? : 420 | } /> 421 | 0 && Object.keys(orderFormValue).length > 0 ? : 422 | } /> 423 | } /> 424 | 425 | 426 | ); 427 | } 428 | 429 | 430 | export default App; -------------------------------------------------------------------------------- /src/assets/images/active-cart-overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/active-cart-overlay.png -------------------------------------------------------------------------------- /src/assets/images/active-hamburger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/active-hamburger.png -------------------------------------------------------------------------------- /src/assets/images/add-to-cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/add-to-cart.png -------------------------------------------------------------------------------- /src/assets/images/arrow-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/arrow-icon.png -------------------------------------------------------------------------------- /src/assets/images/cart-hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/cart-hero.webp -------------------------------------------------------------------------------- /src/assets/images/cart-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/cart-icon.png -------------------------------------------------------------------------------- /src/assets/images/inactive-hamburger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/inactive-hamburger.png -------------------------------------------------------------------------------- /src/assets/images/landing-images/gridFive.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/landing-images/gridFive.webp -------------------------------------------------------------------------------- /src/assets/images/landing-images/gridFour.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/landing-images/gridFour.webp -------------------------------------------------------------------------------- /src/assets/images/landing-images/gridOne.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/landing-images/gridOne.webp -------------------------------------------------------------------------------- /src/assets/images/landing-images/gridThree.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/landing-images/gridThree.webp -------------------------------------------------------------------------------- /src/assets/images/landing-images/gridTwo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/landing-images/gridTwo.webp -------------------------------------------------------------------------------- /src/assets/images/landing-images/hero-cover.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/landing-images/hero-cover.webp -------------------------------------------------------------------------------- /src/assets/images/product-listing-images/category-all-hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/product-listing-images/category-all-hero.webp -------------------------------------------------------------------------------- /src/assets/images/product-listing-images/category-blouses-hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/product-listing-images/category-blouses-hero.webp -------------------------------------------------------------------------------- /src/assets/images/product-listing-images/category-dresses-hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/product-listing-images/category-dresses-hero.webp -------------------------------------------------------------------------------- /src/assets/images/product-listing-images/category-jeans-hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/product-listing-images/category-jeans-hero.webp -------------------------------------------------------------------------------- /src/assets/images/product-listing-images/category-shoes-hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/product-listing-images/category-shoes-hero.webp -------------------------------------------------------------------------------- /src/assets/images/project-preview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Polaris-dev9/Shopping/99b178a5cb042f55968e22c91553ddabcaba0027/src/assets/images/project-preview.webp -------------------------------------------------------------------------------- /src/components/AddToCartButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const AddToCartButton = ({ item, allAttributesAreSelected, selectedAttributes, handleAddProduct, className, 4 | alertMessageMain, 5 | setActiveItem }) => { 6 | return ( 7 |
8 | 25 |
26 | ); 27 | } 28 | 29 | 30 | export default AddToCartButton; 31 | -------------------------------------------------------------------------------- /src/components/ChangeCartItemQuantity.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ChangeCartItemQuantity = ({ handleRemoveProduct, handleAddProduct, singleProduct, className }) => { 4 | return ( 5 |
6 | 16 |

{singleProduct.quantity}

17 | 27 |
28 | ); 29 | } 30 | 31 | export default ChangeCartItemQuantity; -------------------------------------------------------------------------------- /src/components/SuccessMessage.jsx: -------------------------------------------------------------------------------- 1 | const SuccessMessage = () => { 2 | return ( 3 |
4 |

successfully added to bag!

5 |
6 | ); 7 | } 8 | 9 | export default SuccessMessage; 10 | -------------------------------------------------------------------------------- /src/components/attributes/Attributes.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "./attributes.css" 3 | 4 | const Attributes = ({ attribute, handleSelectedAttributes, className }) => { 5 | const [selectedAttribute, setSelectedAttribute] = useState(""); 6 | 7 | return ( 8 |
9 |
10 |

{attribute.id}:

11 | {attribute?.items?.map((item) => ( 12 |
handleSelectedAttributes(attribute.id, item.value)}> 19 | {attribute.id === "Color" ? ( 20 | 27 | ) : ( 28 | 38 | )} 39 |
40 | ))} 41 |
42 |
43 | ); 44 | } 45 | 46 | export default Attributes; 47 | -------------------------------------------------------------------------------- /src/components/attributes/SelectedAttributes.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class SelectedAttributes extends React.Component { 4 | constructor() { 5 | super(); 6 | this.state = { 7 | existingAttributes: [], 8 | selectedAttributes: [], 9 | }; 10 | } 11 | existingAttr = async () => { 12 | await this.props.attribute.items.map((item) => { 13 | return this.setState({ existingAttributes: item.value }); 14 | }); 15 | }; 16 | userSelectedAttr = async () => { 17 | await this.props.userSelectedAttributes.map((item) => { 18 | return this.setState({ selectedAttributes: item.attributeValue }); 19 | }); 20 | }; 21 | 22 | FindTragetAttr = async () => { 23 | await this.props.attribute.items.forEach((existingAttribute) => { 24 | // eslint-disable-next-line array-callback-return 25 | this.props.userSelectedAttributes.map((selectedAttribute) => { 26 | //INSERT USER SELECTED VALUE INTO EXISTING ATTRS 27 | if (this.props.attribute.id === selectedAttribute.attributeId) { 28 | existingAttribute.selectedValue = selectedAttribute.attributeValue; 29 | } 30 | //COMPARE INSERTED/SELECTED VALUE OF EXISTING ATTRS 31 | if (existingAttribute.selectedValue === existingAttribute.value) { 32 | existingAttribute.isSelected = true; 33 | } 34 | }); 35 | }); 36 | }; 37 | 38 | componentDidMount() { 39 | this.userSelectedAttr(); 40 | this.existingAttr(); 41 | this.FindTragetAttr(); 42 | } 43 | render() { 44 | const { attribute, className } = this.props; 45 | return ( 46 |
47 |

{attribute.id}:

48 |
49 | {attribute?.items?.map((item) => ( 50 |
58 | {attribute.id === "Color" ? ( 59 | 67 | ) : ( 68 | 77 | )} 78 |
79 | ))} 80 |
81 |
82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/components/attributes/attributes.css: -------------------------------------------------------------------------------- 1 | .cart-overlay-item-attr .buttons { 2 | flex-direction: row; 3 | gap: 0.313rem; 4 | } 5 | .quick-attribute .buttons { 6 | flex-direction: row; 7 | gap: 0.6rem; 8 | align-items: center; 9 | } 10 | .cart-attr .buttons { 11 | flex-direction: row; 12 | align-items: flex-start; 13 | gap: 0.313rem; 14 | } 15 | 16 | .single-product .attribute .buttons { 17 | flex-direction: row; 18 | gap: 1rem; 19 | } 20 | 21 | .quick-attribute .buttons h3 { 22 | font-weight: 700; 23 | font-size: 1rem; 24 | color: var(--c-text); 25 | font-family: var(--font-roboto); 26 | text-transform: uppercase; 27 | } 28 | 29 | .single-product .attribute h3 { 30 | font-weight: 700; 31 | font-size: 1.5rem; 32 | color: var(--c-text); 33 | font-family: var(--font-roboto); 34 | } 35 | 36 | .attribute-button { 37 | flex-direction: row; 38 | align-items: flex-start; 39 | cursor: pointer; 40 | width: 2.5rem; 41 | height: 2.5rem; 42 | font-weight: 400; 43 | font-size: 1rem; 44 | color: var(--c-text); 45 | text-align: center; 46 | justify-content: center; 47 | align-items: center; 48 | background-color: rgba(255, 255, 255, 0.2); 49 | border: 1px solid var(--c-text); 50 | } 51 | 52 | .single-product .attribute .attribute-button { 53 | flex-direction: row; 54 | align-items: flex-start; 55 | cursor: pointer; 56 | padding: 0.2rem 0.8rem; 57 | font-weight: 400; 58 | font-size: 1rem; 59 | color: var(--c-text); 60 | text-align: center; 61 | justify-content: center; 62 | align-items: center; 63 | background-color: var(--c-white); 64 | border: 1px solid rgb(157, 156, 156); 65 | } 66 | .single-product .attribute .attribute-button:hover { 67 | opacity: 0.8; 68 | } 69 | 70 | .cart-attr .selected-color-box-section { 71 | border-width: 2px; 72 | border-style: solid; 73 | border-color: rgb(157, 156, 156); 74 | width: 1.5rem; 75 | height: 1.5rem; 76 | align-items: center; 77 | justify-content: center; 78 | } 79 | 80 | .cart-attr .unselected-color-box-section { 81 | border-width: 1px; 82 | border-style: solid; 83 | border-color: var(--c-white); 84 | } 85 | 86 | .cart-overlay-item-attr .selected-color-box-section { 87 | border-width: 1px; 88 | border-style: solid; 89 | border-color: var(--c-white); 90 | width: 1.125rem; 91 | height: 1.125rem; 92 | align-items: center; 93 | justify-content: center; 94 | } 95 | 96 | .cart-overlay-item-attr .unselected-color-box-section { 97 | border-width: 1px; 98 | border-style: solid; 99 | border-color: transparent; 100 | } 101 | 102 | .attribute-button.color-attribute { 103 | font-size: 0 !important; 104 | } 105 | -------------------------------------------------------------------------------- /src/components/cart-overlay/CartIcon.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import EmptyCart from "../../assets/images/cart-icon.png"; 3 | import CartOverlay from "./CartOverlay.jsx"; 4 | import { ResetLocation } from "../../helpers/ResetLocation.jsx"; 5 | 6 | const CartIcon = ({ totalPayment, 7 | cartItems, 8 | selectedCurrency, 9 | productsQuantity, 10 | handleAddProduct, 11 | handleRemoveProduct, activeMenu, clearCart, setActiveMenu }) => { 12 | const [toggleCart, setToggleCart] = useState(false); 13 | const cartIcon = useRef(null); 14 | 15 | const toggleCartOverlay = () => { 16 | setToggleCart(!toggleCart) 17 | setActiveMenu(false) 18 | }; 19 | const removeCartOverlay = () => { 20 | setToggleCart(false); 21 | ResetLocation(); 22 | }; 23 | 24 | useEffect(() => { 25 | const handleOutsideClick = (e) => { 26 | if (cartIcon.current && !cartIcon.current.contains(e.target)) { 27 | setToggleCart(false); 28 | } 29 | }; 30 | document.addEventListener("mousedown", handleOutsideClick); 31 | return () => { 32 | document.removeEventListener("mousedown", handleOutsideClick); 33 | }; 34 | }, []); 35 | 36 | return ( 37 |
38 |
39 | empty cart 40 | {productsQuantity > 0 &&

{productsQuantity}

} 41 |
42 | {toggleCart && 43 | 54 | } 55 |
56 | ); 57 | } 58 | 59 | 60 | export default CartIcon; -------------------------------------------------------------------------------- /src/components/cart-overlay/CartOverlay.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import CartOverlayItem from "./CartOverlayItem.jsx"; 4 | import "./cart-overlay.css" 5 | import activeHamburger from "../../assets/images/active-cart-overlay.png"; 6 | 7 | 8 | const CartOverlay = ({ totalPayment, 9 | cartItems, 10 | selectedCurrency, 11 | productsQuantity, 12 | handleAddProduct, 13 | handleRemoveProduct, activeMenu, removeCartOverlay, clearCart }) => { 14 | return ( 15 |
16 | toggle menu removeCartOverlay()} /> 17 | {productsQuantity === 1 ? ( 18 |

19 | My cart, {productsQuantity} items 20 |

21 | ) : ( 22 |

23 | My cart, {productsQuantity} items 24 |

25 | )} 26 | {cartItems.length === 0 ? 27 |

28 | Looks like you haven't added anything to your cart yet 29 |

: 30 | 31 |
32 | {cartItems?.map((singleProduct, index) => ( 33 | 40 | ))} 41 |
42 |
43 |

Total:

44 |

45 | {selectedCurrency} 46 | {totalPayment} 47 |

48 |
49 |
50 | { removeCartOverlay(); }} className="view-bag" to="/cart"> 51 | View cart 52 | 53 | 56 |
57 |
} 58 |
59 | ); 60 | } 61 | 62 | export default CartOverlay; -------------------------------------------------------------------------------- /src/components/cart-overlay/CartOverlayItem.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SelectedAttributes from "../attributes/SelectedAttributes.jsx"; 3 | import ChangeCartItemQuantity from "../ChangeCartItemQuantity.jsx"; 4 | 5 | export default class CartOverlayItem extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | pricing: "", 10 | pricingCurrencySymbol: "", 11 | pricingAmount: "", 12 | priceAmount: "", 13 | }; 14 | this.filterCurrency = this.filterCurrency.bind(this); 15 | } 16 | GetPricing = () => { 17 | this.setState({ 18 | pricing: this.getPrice( 19 | this.props.singleProduct.prices, 20 | this.props.selectedCurrency 21 | ), 22 | }); 23 | }; 24 | filterCurrency = (item, selectedCurrency) => { 25 | const [correctPrice] = item?.prices?.filter((price) => { 26 | return price.currency.symbol === selectedCurrency; 27 | }); 28 | this.setState({ priceAmount: correctPrice.amount.toFixed(2) }); 29 | this.setState({ pricing: correctPrice }); 30 | }; 31 | 32 | getPrice = (prices, currency) => { 33 | const [correctPrice] = prices.filter( 34 | (price) => price.currency.symbol === currency 35 | ); 36 | this.setState({ priceAmount: correctPrice.amount.toFixed(2) }); 37 | return correctPrice; 38 | }; 39 | componentDidMount() { 40 | this.GetPricing(); 41 | this.setState({ 42 | pricingCurrencySymbol: this.state.pricing?.currency?.symbol, 43 | }); 44 | this.setState({ pricingCurrencySymbol: this.state.pricing?.amount }); 45 | } 46 | shouldComponentUpdate(nextProps) { 47 | if (this.props.selectedCurrency !== nextProps.selectedCurrency) { 48 | this.filterCurrency(this.props.singleProduct, nextProps.selectedCurrency); 49 | } 50 | return true; 51 | } 52 | render() { 53 | const { singleProduct, handleAddProduct, handleRemoveProduct } = this.props; 54 | 55 | const { pricing, priceAmount } = this.state; 56 | return ( 57 |
58 |
59 |
60 |
61 |

{singleProduct.name}

62 |

63 | {pricing?.currency?.symbol} 64 | {priceAmount} 65 |

66 |
67 | 68 | {singleProduct?.attributes?.map((attribute) => ( 69 | 76 | ))} 77 |
78 |
79 | 85 | {singleProduct.name} 89 |
90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/components/cart-overlay/cart-overlay.css: -------------------------------------------------------------------------------- 1 | .cart-icon-section { 2 | position: relative; 3 | } 4 | 5 | .cart-overlay { 6 | position: absolute; 7 | background-color: rgba(0, 0, 0, 0.693); 8 | backdrop-filter: blur(2px); 9 | min-width: 30vw; 10 | color: var(--c-white); 11 | z-index: 99; 12 | display: flex; 13 | gap: 1rem; 14 | right: 0; 15 | top: 3.5rem; 16 | padding: 1rem; 17 | flex-direction: column; 18 | justify-content: space-between; 19 | align-items: flex-start; 20 | gap: 2rem; 21 | } 22 | 23 | .cartoverlay-grid { 24 | flex-direction: column; 25 | gap: 4rem; 26 | overflow-y: scroll; 27 | overflow-x: hidden; 28 | max-height: 31.25rem; 29 | width: 100%; 30 | } 31 | 32 | .cartoverlay-product { 33 | flex-direction: row; 34 | justify-content: space-between; 35 | } 36 | .cartoverlay-product-details { 37 | flex-direction: column; 38 | justify-content: space-evenly; 39 | } 40 | 41 | .cartoverlay-product-button { 42 | flex-direction: column; 43 | justify-content: space-between; 44 | } 45 | 46 | .cartoverlay-grid::-webkit-scrollbar { 47 | width: 0.313rem; 48 | } 49 | /* Track */ 50 | .cartoverlay-grid::-webkit-scrollbar-track { 51 | background-color: var(--c-scrollbar-track); 52 | } 53 | /* Handle */ 54 | .cartoverlay-grid::-webkit-scrollbar-thumb { 55 | background-color: var(--c-scrollbar-handle); 56 | } 57 | /* Handle on hover */ 58 | .cartoverlay-grid::-webkit-scrollbar-thumb:hover { 59 | background-color: var(--c-scrollbar-handle-hover); 60 | } 61 | 62 | .cartoverlay-products-single { 63 | display: grid; 64 | grid-template-columns: 1fr 2rem 8rem; 65 | grid-template-rows: 10rem; 66 | gap: 10px; 67 | } 68 | .cart-overlay-item { 69 | flex-direction: row; 70 | align-items: flex-start; 71 | } 72 | .cartoverlay-products-single img { 73 | width: 100%; 74 | height: 100%; 75 | object-fit: cover; 76 | } 77 | .cart-overlay-item-data { 78 | color: var(--c-white); 79 | flex-direction: column; 80 | align-items: flex-start; 81 | gap: 10px; 82 | } 83 | 84 | .product-price { 85 | font-weight: 500; 86 | font-size: 1rem; 87 | } 88 | .all-products-totals { 89 | flex-direction: row; 90 | justify-content: space-between; 91 | font-family: var(--font-roboto); 92 | font-weight: 500; 93 | font-size: 1rem; 94 | } 95 | .all-products-totals :last-child { 96 | font-weight: 700; 97 | } 98 | 99 | .all-products-payment { 100 | display: flex; 101 | flex-direction: row; 102 | gap: 0.75rem; 103 | padding: 1rem; 104 | } 105 | 106 | .all-products-payment a { 107 | text-decoration: none; 108 | cursor: pointer; 109 | padding: 1rem; 110 | font-weight: 400; 111 | font-size: 1rem; 112 | text-decoration: none; 113 | } 114 | 115 | .view-bag { 116 | background-color: transparent; 117 | border-width: 1px; 118 | border-style: solid; 119 | border-color: var(--c-white); 120 | color: var(--c-white); 121 | font-family: var(--font-raleway); 122 | } 123 | 124 | .check-out { 125 | background-color: var(--c-primary); 126 | border-width: 1px; 127 | border-style: solid; 128 | border-color: var(--c-primary); 129 | color: var(--c-white); 130 | } 131 | 132 | .cart-overlay-item-attr { 133 | flex-direction: column; 134 | align-items: flex-start; 135 | gap: 0.5rem; 136 | } 137 | 138 | .cart-overlay-item-attr .selected-attr-box { 139 | height: 1.5rem; 140 | cursor: "pointer"; 141 | background-color: var(--c-white); 142 | color: var(--c-text); 143 | border-width: 1px; 144 | border-style: solid; 145 | border-color: var(--c-white); 146 | font-weight: 400; 147 | font-size: 0.813rem; 148 | align-items: center; 149 | flex-direction: row; 150 | padding: 0.313rem; 151 | } 152 | 153 | .cart-overlay-item-attr .unselected-attr-box { 154 | align-items: center; 155 | flex-direction: row; 156 | padding: 0.313rem; 157 | height: 1.5rem; 158 | cursor: not-allowed; 159 | border-width: 1px; 160 | border-style: solid; 161 | border-color: var(--c-white); 162 | font-weight: 400; 163 | font-size: 0.813rem; 164 | background-color: transparent; 165 | color: var(--c-white); 166 | } 167 | 168 | .cart-overlay-item-attr .selected-color-box { 169 | border-color: var(--c-primary); 170 | border-width: 2px; 171 | border-style: solid; 172 | border: none; 173 | width: 1rem; 174 | height: 1rem; 175 | cursor: "pointer"; 176 | } 177 | 178 | .clear-cart { 179 | background-color: var(--c-primary); 180 | border-width: 1px; 181 | border-style: solid; 182 | border-color: var(--c-primary); 183 | color: var(--c-white); 184 | padding: 1rem; 185 | cursor: pointer; 186 | font-size: 1rem; 187 | font-family: var(--font-raleway); 188 | } 189 | 190 | .cart-overlay-item-attr .unselected-color-box { 191 | border-color: transparent; 192 | border-width: 1px; 193 | border-style: solid; 194 | width: 1rem; 195 | height: 1rem; 196 | cursor: not-allowed; 197 | } 198 | 199 | .cart-overlay-item-attr h3 { 200 | font-weight: 400; 201 | font-size: 0.875rem; 202 | } 203 | 204 | .cartoverlay-product-interaction { 205 | flex-direction: column; 206 | justify-content: space-between; 207 | align-items: center; 208 | font-size: 1rem; 209 | } 210 | 211 | .cartoverlay-product-interaction button { 212 | border-width: 1px; 213 | border-style: solid; 214 | border-color: var(--c-white); 215 | width: 1.5rem; 216 | height: 1.5rem; 217 | background-color: transparent; 218 | cursor: pointer; 219 | color: var(--c-white); 220 | } 221 | 222 | .cart-overlay-item { 223 | align-items: center; 224 | justify-content: space-between; 225 | gap: 1rem; 226 | width: 100%; 227 | height: 100%; 228 | } 229 | 230 | .cart-overlay-item-data { 231 | flex-direction: column; 232 | } 233 | 234 | .cart-overlay-buttons { 235 | height: 15rem; 236 | flex-direction: column; 237 | align-items: center; 238 | justify-content: space-between; 239 | } 240 | 241 | .cart-overlay-button { 242 | cursor: pointer; 243 | background-color: var(--c-white); 244 | color: var(--c-text); 245 | font-size: 20.313rem; 246 | width: 1.875rem; 247 | height: 1.875rem; 248 | border-width: 1px; 249 | border-style: solid; 250 | border-color: var(--c-text); 251 | } 252 | .cart-overlay-button:hover { 253 | border-width: 1px; 254 | border-style: solid; 255 | border-color: var(--c-white); 256 | color: var(--c-white); 257 | background-color: var(--c-text); 258 | } 259 | 260 | .bag-count { 261 | align-items: center; 262 | gap: 0.3rem; 263 | } 264 | 265 | .bag-and-checkout-buttons { 266 | width: 100%; 267 | justify-content: space-evenly; 268 | } 269 | 270 | .cart-overlay-action-buttons { 271 | cursor: pointer; 272 | width: 10rem; 273 | height: 3rem; 274 | border: none; 275 | font-size: 1rem; 276 | font-weight: 700; 277 | } 278 | 279 | .cart-icon { 280 | cursor: pointer; 281 | position: relative; 282 | } 283 | .cart-icon img { 284 | width: 63px; 285 | max-width: 50%; 286 | height: 63px; 287 | object-fit: contain; 288 | } 289 | 290 | .cart-quantity { 291 | display: inline-block; 292 | position: absolute; 293 | top: 0rem; 294 | right: 1.1rem; 295 | flex-direction: column; 296 | align-items: center; 297 | border-radius: 50px; 298 | box-shadow: 0 0 2px #888; 299 | padding: 3px 8px; 300 | background-color: var(--c-white); 301 | color: var(--c-text); 302 | font-size: 0.7rem; 303 | font-weight: 500; 304 | font-family: var(--font-raleway); 305 | } 306 | 307 | .titles-block { 308 | flex-direction: column; 309 | align-items: flex-start; 310 | padding: 0; 311 | gap: 0.313rem; 312 | } 313 | .titles-block h4 { 314 | font-family: var(--font-raleway); 315 | color: var(--white); 316 | font-weight: 800; 317 | } 318 | -------------------------------------------------------------------------------- /src/components/currency-overlay/CurrencyIcon.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import CurrencyOverlay from "./CurrencyOverlay.jsx"; 3 | import "./currency-overlay.css" 4 | 5 | const CurrencyIcon = ({ 6 | selectedCurrency, 7 | allCurrencies, 8 | changeCurrency }) => { 9 | const [dropdownMenu, setDropdownMenu] = useState(false); 10 | const currencyIcon = useRef(null); 11 | 12 | const toggleCurrencyMenu = () => { 13 | setDropdownMenu(!dropdownMenu) 14 | }; 15 | const handleOutsideClick = (e) => { 16 | if (currencyIcon.current && !currencyIcon.current.contains(e.target)) { 17 | setDropdownMenu(false); 18 | } 19 | }; 20 | useEffect(() => { 21 | document.addEventListener("mousedown", handleOutsideClick); 22 | return () => { 23 | document.removeEventListener("mousedown", handleOutsideClick); 24 | }; 25 | }); 26 | 27 | return ( 28 |
33 |

34 | {selectedCurrency}{" "} 35 | 41 | ⌄ 42 | 43 |

44 | {dropdownMenu && ( 45 | 51 | )} 52 |
53 | ); 54 | } 55 | 56 | 57 | export default CurrencyIcon; -------------------------------------------------------------------------------- /src/components/currency-overlay/CurrencyOverlay.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CurrencyOverlay = ({ allCurrencies, changeCurrency }) => { 4 | return ( 5 |
6 | {allCurrencies ? ( 7 | allCurrencies.map((currency, index) => ( 8 |

{ 12 | changeCurrency(currency.symbol); 13 | }} 14 | > 15 | {currency.symbol} {currency.label} 16 |

17 | )) 18 | ) : ( 19 |

Loading...

20 | ) 21 | } 22 |
23 | ); 24 | } 25 | 26 | export default CurrencyOverlay; 27 | -------------------------------------------------------------------------------- /src/components/currency-overlay/currency-overlay.css: -------------------------------------------------------------------------------- 1 | .initial-currency { 2 | flex-direction: row; 3 | cursor: pointer; 4 | align-items: center; 5 | font-family: var(--font-raleway); 6 | justify-content: center; 7 | padding: 0 0.625rem; 8 | gap: 0.75rem; 9 | color: var(--c-text); 10 | font-size: 1.6rem; 11 | } 12 | 13 | .initial-currency p { 14 | display: flex; 15 | flex-direction: row; 16 | justify-content: center; 17 | gap: 0.5rem; 18 | } 19 | 20 | .arrow-icon { 21 | font-size: 1.2rem; 22 | } 23 | 24 | .currency-switcher { 25 | position: absolute; 26 | top: 100%; 27 | flex-direction: column; 28 | background-color: rgba(0, 0, 0, 0.693); 29 | backdrop-filter: blur(2px); 30 | width: max-content; 31 | box-shadow: var(--product-card-box-shadow); 32 | -webkit-box-shadow: var(--product-card-box-shadow); 33 | -moz-box-shadow: var(--product-card-box-shadow); 34 | display: flex; 35 | flex-direction: column; 36 | align-items: stretch; 37 | color: var(--c-white); 38 | } 39 | 40 | .single-currency { 41 | cursor: pointer; 42 | font-family: var(--font-raleway); 43 | font-weight: 500; 44 | font-size: 1rem; 45 | padding: 1.2rem; 46 | } 47 | 48 | .single-currency p { 49 | padding: 1.25rem; 50 | width: 100%; 51 | } 52 | .single-currency:hover { 53 | background-color: #111111; 54 | } 55 | -------------------------------------------------------------------------------- /src/components/header/CategoryMenu.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import { categoriesObj } from "../../data/all-products"; 4 | 5 | const CategoryMenu = ({ changeCategory, activeCategory, closeMenu }) => { 6 | const [allCategories, setAllCategories] = useState([]); 7 | 8 | const getCategories = () => { 9 | setAllCategories(categoriesObj); 10 | }; 11 | useEffect(() => { 12 | getCategories() 13 | }, []); 14 | 15 | return ( 16 | 45 | ); 46 | } 47 | 48 | export default CategoryMenu; 49 | -------------------------------------------------------------------------------- /src/components/header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import CartIcon from "../cart-overlay/CartIcon.jsx"; 4 | import SuccessMessage from "../SuccessMessage.jsx"; 5 | import CategoryMenu from "./CategoryMenu.jsx"; 6 | import CurrencyIcon from "../currency-overlay/CurrencyIcon.jsx"; 7 | import "./header.css"; 8 | import inactiveHamburger from "../../assets/images/inactive-hamburger.png"; 9 | import activeHamburger from "../../assets/images/active-hamburger.png"; 10 | 11 | const Navigation = ({ allCategories, 12 | changeCategory, 13 | activeCategory, 14 | selectedCurrency, 15 | allCurrencies, 16 | changeCurrency, 17 | amountOfItems, 18 | totalPayment, 19 | productsQuantity, 20 | handleRemoveProduct, 21 | handleAddProduct, 22 | cartItems, clearCart, }) => { 23 | 24 | const [activeMenu, setActiveMenu] = useState(false); 25 | 26 | const toggleMenu = () => { 27 | setActiveMenu(!activeMenu); 28 | } 29 | const closeMenu = () => setActiveMenu(false); 30 | 31 | return ( 32 |
33 |
34 | 39 | 40 | 53 | toggle menu 54 |
55 | 93 | 94 |
95 | ); 96 | } 97 | 98 | 99 | export default Navigation; -------------------------------------------------------------------------------- /src/components/header/header.css: -------------------------------------------------------------------------------- 1 | header { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | position: relative; 6 | position: absolute; 7 | top: 0; 8 | width: 100%; 9 | } 10 | .icons-section-active { 11 | display: none; 12 | } 13 | header ul a { 14 | text-decoration: none; 15 | color: var(--c-text); 16 | } 17 | 18 | header ul { 19 | flex-direction: row; 20 | list-style: none; 21 | gap: 1.5rem; 22 | text-transform: uppercase; 23 | align-items: center; 24 | font-family: var(--font-raleway); 25 | font-weight: 400; 26 | font-size: 1rem; 27 | justify-content: center; 28 | } 29 | header ul li a:hover { 30 | color: var(--c-primary); 31 | transition: all ease-in-out 0.3s; 32 | } 33 | nav { 34 | width: 100%; 35 | display: flex; 36 | flex-direction: row; 37 | align-items: flex-start; 38 | justify-content: space-around; 39 | top: 0; 40 | padding: 1rem 0; 41 | background-color: rgba(255, 255, 255, 0.331); 42 | backdrop-filter: blur(5px); 43 | align-items: center; 44 | margin: 0 auto; 45 | z-index: 998; 46 | position: relative; 47 | } 48 | 49 | .header-one { 50 | font-family: var(--font-raleway); 51 | font-weight: 400; 52 | text-decoration: none; 53 | color: var(--c-text); 54 | font-size: 0.8rem; 55 | } 56 | 57 | .icons-section { 58 | display: flex; 59 | flex-direction: row; 60 | align-items: center; 61 | } 62 | 63 | .home-link { 64 | color: var(--c-text); 65 | text-decoration: none; 66 | } 67 | 68 | .active-category-link { 69 | position: relative; 70 | } 71 | .active-category-link li { 72 | color: var(--c-primary); 73 | } 74 | .inactive-category-link li { 75 | color: var(--c-text); 76 | } 77 | .active-category-link::after { 78 | content: ""; 79 | position: absolute; 80 | height: 0.125rem; 81 | background-color: var(--c-primary); 82 | width: max-content; 83 | padding-left: 1rem; 84 | padding-right: 1rem; 85 | transform: translate(-40%, 100%); 86 | left: 40%; 87 | right: 0; 88 | bottom: -50%; 89 | } 90 | .hamburger { 91 | display: none; 92 | } 93 | 94 | @media (max-width: 750px) { 95 | nav { 96 | display: none; 97 | } 98 | .hamburger { 99 | display: inline-block; 100 | cursor: pointer; 101 | object-fit: contain; 102 | width: 50px; 103 | height: 50px; 104 | } 105 | .icons-section { 106 | display: none; 107 | } 108 | .icons-section-active { 109 | display: grid; 110 | grid-template-columns: repeat(3, 3rem); 111 | align-items: center; 112 | position: absolute; 113 | z-index: 997; 114 | right: 0; 115 | justify-content: space-between; 116 | padding: 1rem; 117 | gap: 1rem; 118 | } 119 | .icons-section-active .cart-icon img { 120 | max-width: 100%; 121 | height: auto; 122 | } 123 | .icons-section-active .cart-quantity { 124 | right: -10px; 125 | } 126 | .icons-section-active .initial-currency p { 127 | font-size: 2.5rem; 128 | display: flex; 129 | flex-direction: row; 130 | align-items: flex-start; 131 | } 132 | .icons-section-active .initial-currency span { 133 | font-size: 2.5rem; 134 | } 135 | .icons-section-active .cartoverlay-hamburger { 136 | display: inline-block; 137 | position: absolute; 138 | z-index: 999; 139 | right: 0; 140 | padding: 1rem; 141 | cursor: pointer; 142 | width: 3rem; 143 | height: 3rem; 144 | } 145 | } 146 | .active-menu { 147 | position: fixed; 148 | display: flex; 149 | width: 100%; 150 | height: 100%; 151 | flex-direction: column; 152 | justify-content: initial; 153 | gap: 3rem; 154 | top: 0; 155 | bottom: 0; 156 | padding: 5rem 0; 157 | background-color: rgba(255, 255, 255, 0.616); 158 | backdrop-filter: blur(10px); 159 | align-items: center; 160 | z-index: 996; 161 | } 162 | .active-menu ul { 163 | flex-direction: column; 164 | font-size: 1rem; 165 | } 166 | .active-menu .header-one { 167 | font-size: 1rem; 168 | } 169 | 170 | .icons-section-active .cart-overlay { 171 | position: fixed; 172 | background-color: rgba(0, 0, 0, 0.801); 173 | backdrop-filter: blur(5px); 174 | width: 100%; 175 | height: 100%; 176 | top: 0; 177 | bottom: 0; 178 | color: var(--c-white); 179 | z-index: 998; 180 | display: flex; 181 | flex-direction: column; 182 | justify-content: initial; 183 | align-items: flex-start; 184 | gap: 3rem; 185 | padding: initial; 186 | transform: translate(0, 0); 187 | right: 0; 188 | left: 0; 189 | } 190 | 191 | .cart-overlay .overlay-title, 192 | .cart-overlay .all-products-totals, 193 | .cart-overlay .cart-overlay-item-data { 194 | padding: 1rem; 195 | } 196 | .cartoverlay-hamburger { 197 | display: none; 198 | } 199 | -------------------------------------------------------------------------------- /src/core-ui/hovers.css: -------------------------------------------------------------------------------- 1 | /* TRANSITIONS */ 2 | .order-btn, 3 | .active-add-to-cart, 4 | .cartoverlay-product-interaction button, 5 | .cart-product-interaction button, 6 | .view-bag, 7 | .check-out, 8 | .clear-cart, 9 | .single-currency, 10 | .big-picture, 11 | .big-picture.zoom-image, 12 | .cart-overlay-button { 13 | transition: all ease-in-out 0.2s; 14 | } 15 | 16 | @media (hover: hover) { 17 | .order-btn:hover, 18 | .active-add-to-cart:hover .cartoverlay-product-interaction button:hover, 19 | .cart-product-interaction button:hover, 20 | .view-bag:hover, 21 | .check-out:hover, 22 | .clear-cart:hover, 23 | .single-currency:hover { 24 | transition: all ease-in-out 0.2s; 25 | } 26 | .order-btn:hover, 27 | .active-add-to-cart:hover { 28 | opacity: 0.7; 29 | } 30 | 31 | .cartoverlay-product-interaction button:hover, 32 | .cart-product-interaction button:hover { 33 | color: var(--c-primary); 34 | border-color: var(--c-primary); 35 | } 36 | .view-bag:hover { 37 | color: var(--c-primary); 38 | border-color: var(--c-primary); 39 | } 40 | .check-out:hover, 41 | .clear-cart:hover { 42 | color: var(--c-primary); 43 | border-color: var(--c-primary); 44 | background-color: transparent; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/core-ui/responsive.css: -------------------------------------------------------------------------------- 1 | @media screen and (max-width: 1201px) { 2 | .store-products { 3 | grid-template-columns: repeat(2, 1fr); 4 | } 5 | .single-product { 6 | gap: 1rem; 7 | } 8 | } 9 | 10 | @media screen and (max-width: 1025px) { 11 | .single-product { 12 | flex-direction: column; 13 | } 14 | } 15 | 16 | @media screen and (max-width: 801px) { 17 | header { 18 | gap: 0; 19 | } 20 | 21 | .store-products { 22 | grid-template-columns: 1fr; 23 | } 24 | 25 | .product-showcase { 26 | flex-direction: column-reverse; 27 | } 28 | .big-picture { 29 | grid-column-start: 1; 30 | grid-row-start: 1; 31 | } 32 | .mini-pictures { 33 | flex-direction: row; 34 | width: 100%; 35 | overflow-x: scroll; 36 | gap: 2rem; 37 | } 38 | 39 | .grid-one, 40 | .grid-two, 41 | .grid-three, 42 | .grid-four, 43 | .grid-five { 44 | height: 20rem; 45 | } 46 | .grid-four .grid-button { 47 | right: -50%; 48 | } 49 | .grid-five .grid-button { 50 | right: -70%; 51 | } 52 | .landing-hero { 53 | min-height: 30rem; 54 | } 55 | } 56 | 57 | @media screen and (max-width: 600px) { 58 | .cart-content { 59 | flex-direction: column-reverse; 60 | } 61 | .cart-product-interaction { 62 | flex-direction: row; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/core-ui/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-raleway: "Poppins", sans-serif; 3 | --font-roboto: "Roboto", sans-serif; 4 | --sans-pro: "Source Sans Pro", sans-serif; 5 | --c-text: rgb(28, 30, 33); 6 | --c-primary: rgb(240, 97, 97); 7 | --c-white: rgb(255, 255, 255); 8 | --c-gray: rgb(141, 143, 154); 9 | --product-card-box-shadow: 0px 40px 35px rgba(168, 172, 176, 0.19); 10 | --c-scrollbar-track: rgb(241, 241, 241); 11 | --c-scrollbar-handle: rgba(136, 136, 136, 0.18); 12 | --c-scrollbar-handle-hover: rgb(85, 85, 85); 13 | } 14 | 15 | * { 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | body { 21 | -ms-overflow-style: none; /* for Internet Explorer, Edge */ 22 | scrollbar-width: none; /* for Firefox */ 23 | overflow-y: scroll; 24 | box-sizing: border-box; 25 | } 26 | body::-webkit-scrollbar { 27 | display: none; /* for Chrome, Safari, and Opera */ 28 | } 29 | image { 30 | display: block; 31 | } 32 | ul, 33 | ol, 34 | li { 35 | padding: 0; 36 | margin: 0; 37 | } 38 | 39 | main { 40 | display: flex; 41 | flex-direction: column; 42 | } 43 | 44 | .single-product-titles .brand, 45 | .single-product-titles .name, 46 | .single-product-price p, 47 | .single-product-pricing .product-price, 48 | .cartoverlay-products h4, 49 | .cartoverlay-products .empty-cart, 50 | .cartoverlay .payment button, 51 | .cart-overlay-item-attr h3, 52 | .cart h2, 53 | .cart-products-single .cart-data h2, 54 | .cart-item-pricing .product-price, 55 | .cart-product-interaction p, 56 | .cart-totals .cart-totals-sum, 57 | .cart-totals p, 58 | .order-btn { 59 | font-family: var(--font-raleway); 60 | } 61 | .cart-attr .selected-attr-box, 62 | .cart-attr .unselected-attr-box, 63 | .cart-overlay-item-attr .selected-attr-box, 64 | .cart-overlay-item-attr .unselected-attr-box, 65 | .single-product .attribute .attribute-button, 66 | .quick-attribute .attribute-button { 67 | font-family: var(--sans-pro); 68 | } 69 | 70 | header, 71 | header ul, 72 | .initial-currency, 73 | .currency-switcher, 74 | .product, 75 | .titles-block, 76 | .cart-quantity, 77 | .product-card, 78 | .quick-addto-cart, 79 | .quick-attribute, 80 | .quick-addtocart .active-add-to-cart, 81 | .quick-addtocart .inactive-add-to-cart, 82 | .quick-attribute .attribute-button, 83 | .quick-attribute .buttons, 84 | .active-add-to-cart, 85 | .inactive-add-to-cart, 86 | .product-showcase, 87 | .mini-pictures, 88 | .single-product .data, 89 | .single-product-price, 90 | .single-product .attribute, 91 | .single-product .attribute .buttons, 92 | .pricing-section, 93 | .single-product .attribute .buttons, 94 | .single-product .attribute .attribute-button, 95 | .single-product .description, 96 | .cartoverlay-products, 97 | .cartoverlay-grid, 98 | .cartoverlay-product, 99 | .cartoverlay-product-details, 100 | .cartoverlay-product-button, 101 | .cart-overlay-item-data, 102 | .cart-overlay-item, 103 | .cart-overlay-item-attr, 104 | .cart-overlay-item-attr .selected-attr-box, 105 | .cart-overlay-item-attr .unselected-attr-box, 106 | .cart-overlay-item-attr .selected-color-box-section, 107 | .cart-overlay-item-attr .buttons, 108 | .cartoverlay-product-interaction, 109 | .cart-overlay-item, 110 | .cart-overlay-item-data, 111 | .cart-overlay-buttons, 112 | .bag-count, 113 | .bag-and-checkout-buttons, 114 | .cart, 115 | .cart-products-single, 116 | .cart-products-single .cart-data, 117 | .cart-attr, 118 | .cart-attr .selected-attr-box, 119 | .cart-attr .unselected-attr-box, 120 | .cart-attr .selected-color-box-section, 121 | .cart-attr .buttons, 122 | .cart-content, 123 | .cart-product-interaction, 124 | .cart-product-button, 125 | .cart-totals, 126 | .cart-totals section { 127 | display: flex; 128 | } 129 | 130 | .cart-hero h2 { 131 | position: absolute; 132 | z-index: 3; 133 | font-family: var(--font-raleway); 134 | top: 50%; 135 | font-size: 4rem; 136 | letter-spacing: 1px; 137 | text-transform: uppercase; 138 | text-align: center; 139 | color: var(--c-white); 140 | } 141 | .active-add-to-cart { 142 | flex-direction: column; 143 | align-items: center; 144 | padding: 1rem 2rem; 145 | width: 100%; 146 | height: 3.25rem; 147 | color: var(--c-white); 148 | border: none; 149 | font-family: var(--font-raleway); 150 | font-weight: 600; 151 | font-size: 1rem; 152 | cursor: pointer; 153 | background-color: var(--c-primary); 154 | } 155 | .inactive-add-to-cart { 156 | flex-direction: column; 157 | align-items: center; 158 | padding: 1rem 2rem; 159 | width: 100%; 160 | height: 3.25rem; 161 | color: gray; 162 | border: none; 163 | font-family: var(--font-raleway); 164 | font-weight: 600; 165 | font-size: 1rem; 166 | cursor: not-allowed; 167 | opacity: 0.8; 168 | } 169 | 170 | .success-alert { 171 | display: none; 172 | } 173 | .success-alert.visible { 174 | display: inline-block; 175 | position: fixed; 176 | top: 10%; 177 | right: 5%; 178 | background-color: rgba(219, 219, 219, 0.22); 179 | backdrop-filter: blur(5px); 180 | padding: 0.7rem; 181 | border-radius: 10px; 182 | } 183 | .success-alert.visible p { 184 | font-family: var(--font-raleway); 185 | font-size: 1rem; 186 | color: var(--c-text); 187 | font-weight: 500; 188 | } 189 | -------------------------------------------------------------------------------- /src/data/all-products.js: -------------------------------------------------------------------------------- 1 | const categoriesObj = [ 2 | { 3 | "id": "all", 4 | "name": "all" 5 | }, 6 | { 7 | "id": "dresses", 8 | "name": "dresses" 9 | }, 10 | { 11 | "id": "blouses", 12 | "name": "blouses" 13 | }, 14 | { 15 | "id": "jeans", 16 | "name": "jeans" 17 | }, 18 | { 19 | "id": "shoes", 20 | "name": "shoes" 21 | } 22 | ]; 23 | 24 | 25 | 26 | export { categoriesObj }; -------------------------------------------------------------------------------- /src/database/firebase.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getFirestore} from 'firebase/firestore/lite'; 3 | const API_KEY = import.meta.env.VITE_API_KEY; 4 | const AUTH_DOMAIN = import.meta.env.VITE_AUTH_DOMAIN 5 | const PROJECT_ID = import.meta.env.VITE_PROJECT_ID 6 | const STORAGE_BUCKET = import.meta.env.VITE_STORAGE_BUCKET 7 | const MESSAGING_ID = import.meta.env.VITE_MESSAGING_ID 8 | const APP_ID = import.meta.env.VITE_APP_ID 9 | const MEASUREMENT_ID = import.meta.env.VITE_MEASUREMENT_ID 10 | 11 | const firebaseConfig = { 12 | apiKey: API_KEY, 13 | authDomain: AUTH_DOMAIN, 14 | projectId: PROJECT_ID, 15 | storageBucket: STORAGE_BUCKET, 16 | messagingSenderId: MESSAGING_ID, 17 | appId: APP_ID, 18 | measurementId: MEASUREMENT_ID 19 | }; 20 | 21 | // Initialize Firebase 22 | const app = initializeApp(firebaseConfig); 23 | const products_database = getFirestore(app); 24 | export default products_database; -------------------------------------------------------------------------------- /src/helpers/ResetLocation.jsx: -------------------------------------------------------------------------------- 1 | export const ResetLocation = () => window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import "./core-ui/styles.css"; 5 | import "./core-ui/hovers.css"; 6 | import "./core-ui/responsive.css"; 7 | import "./routes/single-product/single-product.css"; 8 | 9 | const root = ReactDOM.createRoot(document.getElementById("root")); 10 | root.render(); 11 | -------------------------------------------------------------------------------- /src/routes/all-products/AllProducts.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Product from "./Product.jsx"; 3 | import CategoryAllHero from '../../assets/images/product-listing-images/category-all-hero.webp' 4 | import CategoryDressesHero from '../../assets/images/product-listing-images/category-dresses-hero.webp' 5 | import CategoryBlousesHero from '../../assets/images/product-listing-images/category-blouses-hero.webp' 6 | import CategoryShoesHero from '../../assets/images/product-listing-images/category-shoes-hero.webp' 7 | import CategoryJeansHero from '../../assets/images/product-listing-images/category-jeans-hero.webp' 8 | import "./all-products.css"; 9 | 10 | 11 | const AllProducts = ({ allProducts, 12 | activeCategory, 13 | selectedCurrency, 14 | handleAddProduct, 15 | alertMessageMain, 16 | productId, 17 | isLoading 18 | }) => { 19 | 20 | const [quickAddToCartVisible, setQuickAddToCartVisible] = useState(false); 21 | const [activeItem, setActiveItem] = useState(null); 22 | 23 | useEffect(() => { 24 | document.title = `${activeCategory.charAt(0).toUpperCase() + activeCategory.slice(1)} | Shopping Time`; 25 | }, [activeCategory]); 26 | 27 | const toggleQuickCart = () => { 28 | setQuickAddToCartVisible(!quickAddToCartVisible); 29 | } 30 | const removeQuickAddToCart = () => { 31 | setQuickAddToCartVisible(false); 32 | }; 33 | 34 | return ( 35 |
36 |
37 |

{activeCategory === 'all' ? "Boost your style sense!" : activeCategory === 'dresses' ? "Let's create your own style" : activeCategory === 'blouses' ? "The joy of dressing" : activeCategory === 'shoes' ? "Unlock your style" : activeCategory === 'jeans' ? "Fashion never sleeps" : "Boost your style sense!"}

38 | 39 | 40 |
41 | 42 |
43 |

{activeCategory}{activeCategory === 'all' && ' Products'}

44 | 45 | {isLoading ? 46 |
47 |

Products are loading, please wait...

48 |
: 49 |
50 | {allProducts 51 | && allProducts.map((item) => ( 52 | 67 | ))} 68 |
} 69 | 70 |
71 |
72 | ); 73 | } 74 | 75 | 76 | export default AllProducts; -------------------------------------------------------------------------------- /src/routes/all-products/Product.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import addToCart from "../../assets/images/add-to-cart.png"; 4 | import QuickAddToCart from './QuickAddToCart' 5 | 6 | const Product = ({ 7 | selectedCurrency, item, handleAddProduct, alertMessageMain, toggleQuickCart, removeQuickAddToCart 8 | , quickAddToCartVisible, setActiveItem, activeItem 9 | }) => { 10 | const [pricing, setPricing] = useState(""); 11 | const [selectedAttributes, setSelectedAttributes] = useState([]); 12 | const [allAttributesAreSelected, setAllAttributesAreSelected] = useState(false); 13 | const [priceAmount, setPriceAmount] = useState(""); 14 | const [imageShadow, setImageShadow] = useState(false); 15 | 16 | const filterCurrency = (item, selectedCurrency) => { 17 | const correctPrice = item?.prices?.find( 18 | (price) => price.currency.symbol === selectedCurrency 19 | ); 20 | 21 | if (correctPrice) { 22 | setPriceAmount(correctPrice.amount.toFixed(2)); 23 | setPricing(correctPrice); 24 | } 25 | }; 26 | 27 | const handleSelectedAttributes = (attributeId, attributeValue) => { 28 | const newSelectedAttribute = { attributeId, attributeValue }; 29 | const updatedAttributes = selectedAttributes.map(attribute => 30 | attribute.attributeId === newSelectedAttribute.attributeId 31 | ? { ...newSelectedAttribute } 32 | : attribute 33 | ); 34 | 35 | if (!updatedAttributes.some(attribute => attribute.attributeId === newSelectedAttribute.attributeId)) { 36 | updatedAttributes.push(newSelectedAttribute); 37 | } 38 | 39 | setSelectedAttributes(updatedAttributes); 40 | }; 41 | 42 | const handleAllAttributesAreSelected = () => { 43 | setAllAttributesAreSelected(true); 44 | }; 45 | 46 | const handleProductHasNoAttributes = () => { 47 | if (item.attributes.length === 0) { 48 | handleAllAttributesAreSelected(); 49 | } 50 | }; 51 | 52 | useEffect(() => { 53 | handleProductHasNoAttributes(); 54 | }, []); 55 | 56 | useEffect(() => { 57 | handleProductHasNoAttributes(); 58 | }, []); 59 | 60 | useEffect(() => { 61 | filterCurrency(item, selectedCurrency); 62 | }, [item, selectedCurrency]); 63 | 64 | useEffect(() => { 65 | if (selectedAttributes.length === item.attributes.length) { 66 | handleAllAttributesAreSelected(); 67 | } 68 | }, [selectedAttributes, item.attributes.length]); 69 | 70 | return ( 71 |
{ 75 | setImageShadow(true); 76 | }} 77 | onMouseLeave={() => { 78 | setImageShadow(false); 79 | }} 80 | > 81 | 82 |
83 | {!item.inStock && ( 84 |

OUT OF STOCK

85 | )} 86 |
90 |
91 | 92 |
93 |

94 | {item.brand} {item.name} 95 |

96 |

97 | {pricing?.currency?.symbol} 98 | {priceAmount} 99 |

100 |
101 | {!item.inStock ? null : activeItem === item.id ? 102 | : 114 | setActiveItem(item.id)} 118 | alt="Add to cart icon" 119 | />} 120 |
121 | ); 122 | } 123 | export default Product; -------------------------------------------------------------------------------- /src/routes/all-products/QuickAddToCart.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Attribute from "../../components/attributes/Attributes.jsx"; 3 | import AddToCartButton from "../../components/AddToCartButton.jsx"; 4 | 5 | 6 | const QuickAddToCart = ({ item, 7 | handleAddProduct, 8 | handleSelectedAttributes, 9 | selectedAttributes, 10 | allAttributesAreSelected, 11 | alertMessageMain, toggleQuickCart, setActiveItem }) => { 12 | return ( 13 |
14 | { 15 | item?.attributes?.map((attribute) => ( 16 | 23 | )) 24 | } 25 | 35 |

setActiveItem(null)}> 36 | close 37 |

38 |
39 | ); 40 | } 41 | 42 | 43 | export default QuickAddToCart; -------------------------------------------------------------------------------- /src/routes/all-products/all-products.css: -------------------------------------------------------------------------------- 1 | .products-hero { 2 | height: 70vh; 3 | position: relative; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | } 8 | .products-hero h2 { 9 | position: absolute; 10 | z-index: 3; 11 | font-family: var(--font-raleway); 12 | top: 50%; 13 | font-size: 4rem; 14 | letter-spacing: 1px; 15 | text-transform: uppercase; 16 | text-align: center; 17 | color: var(--c-white); 18 | } 19 | .products-hero img { 20 | position: absolute; 21 | width: 100%; 22 | height: 90%; 23 | object-fit: cover; 24 | object-position: center; 25 | z-index: 2; 26 | } 27 | .products-loader { 28 | height: 100vh; 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | } 33 | .products-loader h3 { 34 | font-size: 2rem; 35 | text-align: center; 36 | font-weight: 400; 37 | padding-top: 5rem; 38 | } 39 | 40 | .product-listing-page { 41 | margin-top: 3rem; 42 | } 43 | .product-listing-page .active-category { 44 | text-align: center; 45 | color: var(--c-text); 46 | font-family: var(--font-raleway); 47 | text-transform: capitalize; 48 | font-size: 3.5rem; 49 | letter-spacing: 1px; 50 | padding: 2rem; 51 | } 52 | 53 | .store-products { 54 | display: grid; 55 | grid-template-columns: repeat(3, 1fr); 56 | margin-bottom: 10rem; 57 | column-gap: 2rem; 58 | row-gap: 6rem; 59 | padding: 3rem; 60 | } 61 | .product { 62 | position: relative; 63 | flex-direction: column; 64 | height: 28rem; 65 | } 66 | 67 | .product-card { 68 | position: relative; 69 | flex-direction: column; 70 | gap: 1.5rem; 71 | min-height: 27rem; 72 | padding: 1rem; 73 | } 74 | .product-card.product-shadow { 75 | box-shadow: var(--product-card-box-shadow); 76 | -webkit-box-shadow: var(--product-card-box-shadow); 77 | -moz-box-shadow: var(--product-card-box-shadow); 78 | background-color: var(--c-white); 79 | } 80 | 81 | .product-card .brand-name, 82 | .product-card .product-price { 83 | font-family: var(--font-raleway); 84 | } 85 | .product-card .brand-name { 86 | font-weight: 300; 87 | font-size: 1.125rem; 88 | } 89 | .product-card .product-price { 90 | font-weight: 500; 91 | font-size: 1.125rem; 92 | } 93 | 94 | .item-preview { 95 | text-decoration: none; 96 | } 97 | 98 | .img-container { 99 | display: flex; 100 | width: 100%; 101 | height: 21.125rem; 102 | justify-content: center; 103 | } 104 | 105 | .img-container img { 106 | width: 100%; 107 | object-fit: cover; 108 | height: 100%; 109 | margin: auto; 110 | display: block; 111 | } 112 | 113 | .out-of-stock-sign { 114 | position: absolute; 115 | transform: (translate(50%, 50%)); 116 | top: 50%; 117 | left: 0; 118 | right: 0; 119 | text-align: center; 120 | font-weight: 400; 121 | font-size: 1.5rem; 122 | color: var(--c-gray); 123 | text-transform: uppercase; 124 | font-family: var(--font-raleway); 125 | } 126 | 127 | .item-preview-img { 128 | width: 100%; 129 | height: 330px; 130 | -webkit-background-size: contain; 131 | -moz-background-size: contain; 132 | -o-background-size: contain; 133 | background-size: contain; 134 | background-position: center center; 135 | background-repeat: no-repeat; 136 | } 137 | 138 | .quick-buy { 139 | position: absolute; 140 | right: 3rem; 141 | bottom: 3rem; 142 | cursor: pointer; 143 | background-color: var(--c-primary); 144 | border-radius: 50%; 145 | width: 1.5rem; 146 | height: 1.5rem; 147 | padding: 0.8rem; 148 | } 149 | 150 | .quick-addto-cart { 151 | position: absolute; 152 | top: 0; 153 | left: 0; 154 | right: 0; 155 | flex-direction: column; 156 | justify-content: space-evenly; 157 | width: inherit; 158 | background-color: rgba(255, 255, 255, 0.22); 159 | backdrop-filter: blur(5px); 160 | height: 100%; 161 | align-items: center; 162 | } 163 | 164 | .quick-attribute { 165 | flex-direction: column; 166 | align-items: center; 167 | padding: 0.5rem; 168 | } 169 | 170 | .quick-addtocart .active-add-to-cart { 171 | flex-direction: column; 172 | align-items: center; 173 | padding: 1rem 2rem; 174 | width: 18.25rem; 175 | height: 3.25rem; 176 | color: var(--c-white); 177 | border: none; 178 | font-family: var(--font-raleway); 179 | font-weight: 600; 180 | font-size: 1rem; 181 | cursor: pointer; 182 | background-color: var(--c-primary); 183 | } 184 | .quick-addtocart .inactive-add-to-cart { 185 | flex-direction: column; 186 | align-items: center; 187 | padding: 1rem 2rem; 188 | width: 18.25rem; 189 | height: 3.25rem; 190 | color: var(--c-text); 191 | background-color: rgba(255, 255, 255, 0.22); 192 | border: none; 193 | font-family: var(--font-raleway); 194 | font-weight: 600; 195 | font-size: 1rem; 196 | cursor: not-allowed; 197 | opacity: 0.6; 198 | border: 1px solid var(--c-text); 199 | } 200 | 201 | .close-quickbuy { 202 | text-align: center; 203 | color: var(--c-text); 204 | text-decoration: underline; 205 | cursor: pointer; 206 | letter-spacing: 1px; 207 | font-family: var(--font-raleway); 208 | font-weight: 500; 209 | } 210 | 211 | @media (hover: hover) { 212 | .quick-buy:hover { 213 | background-color: rgb(241, 56, 56); 214 | } 215 | } 216 | 217 | @media (max-width: 750px) { 218 | .products-hero, 219 | .products-hero img { 220 | height: 100vh; 221 | } 222 | .products-hero h2 { 223 | top: 20%; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/routes/cart/Cart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import CartSingleItem from "./CartSingleItem.jsx"; 3 | import CartTotals from "./CartTotals.jsx"; 4 | import "./cart.css"; 5 | import CartHero from "../../assets/images/cart-hero.webp" 6 | 7 | const Cart = ({ selectedCurrency, 8 | totalPayment, 9 | taxes, 10 | handleRemoveProduct, 11 | cartItems, 12 | handleAddProduct, 13 | selectedAttributes, 14 | productsQuantity, clearCart }) => { 15 | useEffect(() => { 16 | document.title = "Cart | Shopping Time"; 17 | }, []); 18 | return ( 19 |
20 |
21 |

Cart

22 | 23 |
24 |
25 | {cartItems.length === 0 ? ( 26 |
27 |

28 | Looks like you haven't added anything to your cart yet. 29 |

30 |
31 | ) : ( 32 |
33 | {cartItems.map((singleProduct) => { 34 | return ( 35 | 43 | ); 44 | })} 45 | 46 | 53 |
54 | )} 55 |
56 |
57 | ); 58 | } 59 | 60 | export default Cart; 61 | -------------------------------------------------------------------------------- /src/routes/cart/CartSingleItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import SelectedAttributes from "../../components/attributes/SelectedAttributes.jsx"; 3 | import SimpleImageSlider from "react-simple-image-slider"; 4 | import ChangeCartItemQuantity from "../../components/ChangeCartItemQuantity.jsx"; 5 | 6 | 7 | const CartSingleItem = ({ selectedCurrency, singleProduct, handleAddProduct, handleRemoveProduct }) => { 8 | const [priceAmount, setPriceAmount] = useState(""); 9 | 10 | useEffect(() => { 11 | const targetCurrency = singleProduct.prices.filter((price) => price.currency.symbol === selectedCurrency)[0]; 12 | setPriceAmount(targetCurrency.amount.toFixed(2)); 13 | }, [selectedCurrency, singleProduct.prices]); 14 | 15 | return ( 16 |
17 |
18 |

{singleProduct.brand}

19 |

{singleProduct.name}

20 | 21 |
22 |

23 | {selectedCurrency} 24 | {priceAmount} 25 |

26 |
27 | {singleProduct?.attributes?.map((attribute) => ( 28 | 35 | ))} 36 |
37 |
38 | 44 | {singleProduct.gallery.length === 1 ? ( 45 | 52 | ) : ( 53 | 63 | )} 64 |
65 |
66 | ); 67 | } 68 | 69 | export default CartSingleItem; 70 | -------------------------------------------------------------------------------- /src/routes/cart/CartTotals.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const CartTotals = ({ totalPayment, productsQuantity, selectedCurrency, taxes, clearCart }) => { 5 | return ( 6 |
7 |

Totals

10 |
    11 |
  • Tax 21%: {selectedCurrency} 12 | {taxes}
  • 13 |
  • Quantity: {productsQuantity}
  • 14 |
  • Total: {selectedCurrency} 15 | {totalPayment}
  • 16 |
17 |
18 | Continue shopping 19 | Checkout 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | export default CartTotals; 27 | -------------------------------------------------------------------------------- /src/routes/cart/cart.css: -------------------------------------------------------------------------------- 1 | .cart-hero { 2 | height: 40vh; 3 | position: relative; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | } 9 | 10 | .cart-hero h2 { 11 | position: absolute; 12 | z-index: 3; 13 | font-family: var(--font-raleway); 14 | top: 50%; 15 | font-size: 4rem; 16 | letter-spacing: 1px; 17 | text-transform: uppercase; 18 | text-align: center; 19 | color: black; 20 | } 21 | 22 | .cart-hero img { 23 | position: absolute; 24 | width: 100%; 25 | height: 100%; 26 | object-fit: cover; 27 | z-index: 2; 28 | } 29 | 30 | .cart { 31 | padding-top: 5rem; 32 | flex-direction: column; 33 | margin: 0 auto; 34 | width: 90%; 35 | margin-bottom: 3.5rem; 36 | gap: 4.6rem; 37 | } 38 | 39 | .cart-item-section { 40 | display: flex; 41 | flex-direction: column; 42 | gap: 2.8rem; 43 | } 44 | 45 | .cart .empty-cart { 46 | text-align: center; 47 | font-size: 1.5rem; 48 | } 49 | 50 | .cart-products-single { 51 | flex-direction: row; 52 | justify-content: space-between; 53 | } 54 | 55 | .cart-products-single .cart-data { 56 | flex-direction: column; 57 | gap: 0.7rem; 58 | } 59 | 60 | .cart-products-single .cart-data h2 { 61 | font-size: 1.875rem; 62 | } 63 | 64 | .cart-products-single .product-brand { 65 | font-weight: 600; 66 | } 67 | 68 | .cart-products-single .product-name { 69 | font-weight: 400; 70 | } 71 | 72 | .cart-item-pricing .product-price { 73 | font-weight: 700; 74 | font-size: 1.5rem; 75 | } 76 | 77 | .cart-attr { 78 | flex-direction: column; 79 | gap: 0.313rem; 80 | } 81 | 82 | div.rsis-container { 83 | max-width: 100%; 84 | } 85 | 86 | .cart-attr .selected-attr-box { 87 | flex-direction: row; 88 | align-items: flex-start; 89 | cursor: pointer; 90 | padding: 0.2rem 0.8rem; 91 | font-weight: 400; 92 | font-size: 1rem; 93 | color: var(--c-white); 94 | text-align: center; 95 | justify-content: center; 96 | align-items: center; 97 | background-color: rgb(0, 0, 0); 98 | border: 1px solid rgb(0, 0, 0); 99 | border-radius: 5px; 100 | } 101 | 102 | .cart-attr .unselected-attr-box { 103 | flex-direction: row; 104 | align-items: flex-start; 105 | cursor: not-allowed; 106 | padding: 0.2rem 0.8rem; 107 | font-weight: 400; 108 | font-size: 1rem; 109 | color: var(--c-text); 110 | text-align: center; 111 | justify-content: center; 112 | align-items: center; 113 | background-color: var(--c-white); 114 | border: 1px solid rgb(157, 156, 156); 115 | border-radius: 5px; 116 | } 117 | 118 | .cart-attr .selected-color-box { 119 | border-color: var(--c-white); 120 | border-width: 2px; 121 | border-style: solid; 122 | border: none; 123 | width: 1.5rem; 124 | height: 1.5rem; 125 | cursor: pointer; 126 | } 127 | 128 | .cart-attr .unselected-color-box { 129 | border-color: transparent; 130 | border-width: 1px; 131 | border-style: solid; 132 | width: 1.5rem; 133 | height: 1.5rem; 134 | cursor: not-allowed; 135 | } 136 | 137 | .cart-attr h3 { 138 | font-family: var(--font-roboto); 139 | font-weight: 700; 140 | font-size: 1.125rem; 141 | text-transform: uppercase; 142 | } 143 | 144 | .cart-content { 145 | flex-direction: row; 146 | gap: 1.3rem; 147 | } 148 | 149 | .cart-product-interaction { 150 | flex-direction: column; 151 | justify-content: space-between; 152 | align-items: center; 153 | font-size: 1rem; 154 | } 155 | 156 | .cart-product-interaction button { 157 | border-width: 1px; 158 | border-style: solid; 159 | border-color: var(--c-text); 160 | height: 2rem; 161 | width: 2rem; 162 | background-color: transparent; 163 | cursor: pointer; 164 | font-size: 1.3rem; 165 | } 166 | 167 | .cart-product-interaction p { 168 | font-weight: 500; 169 | font-size: 1.5rem; 170 | } 171 | 172 | .cart-product-button { 173 | flex-direction: column; 174 | justify-content: space-between; 175 | } 176 | 177 | .cart-totals { 178 | flex-direction: column; 179 | gap: 2rem; 180 | } 181 | 182 | .cart-totals h3 { 183 | font-family: var(--font-raleway); 184 | font-size: 1.5rem; 185 | display: flex; 186 | flex-direction: row; 187 | gap: 1rem; 188 | } 189 | 190 | .clear-cart-totals { 191 | border: none; 192 | text-decoration: underline; 193 | cursor: pointer; 194 | background-color: transparent; 195 | transition: all ease-in-out 0.3s; 196 | } 197 | 198 | .cart-totals ul { 199 | display: flex; 200 | gap: 1rem; 201 | list-style: none; 202 | flex-direction: row; 203 | justify-content: space-between; 204 | width: max-content; 205 | border: 1px solid black; 206 | padding: 1rem; 207 | } 208 | 209 | .cart-totals ul::before { 210 | display: block; 211 | background-color: black; 212 | height: 2px; 213 | width: 100%; 214 | } 215 | 216 | .cart-totals ul li { 217 | display: flex; 218 | flex-direction: row; 219 | gap: 1rem; 220 | font-weight: 600; 221 | } 222 | 223 | .cart-totals ul li span { 224 | font-weight: 400; 225 | } 226 | 227 | .cart-totals p { 228 | font-weight: 700; 229 | font-size: 1.5rem; 230 | } 231 | 232 | .cart-buttons { 233 | display: flex; 234 | flex-direction: row; 235 | gap: 2rem; 236 | } 237 | 238 | .order-btn { 239 | text-decoration: none; 240 | cursor: pointer; 241 | padding: 1rem; 242 | font-weight: 400; 243 | font-size: 1rem; 244 | text-decoration: none; 245 | background-color: var(--c-primary); 246 | border-width: 1px; 247 | border-style: solid; 248 | border-color: var(--c-primary); 249 | color: var(--c-white); 250 | width: max-content; 251 | } 252 | 253 | .cart-go-back { 254 | text-decoration: none; 255 | cursor: pointer; 256 | padding: 1rem; 257 | flex-direction: column; 258 | align-items: center; 259 | width: max-content; 260 | color: gray; 261 | border: 1px solid gray; 262 | font-family: var(--font-raleway); 263 | font-weight: 600; 264 | font-size: 1rem; 265 | opacity: 0.8; 266 | } 267 | 268 | .cart-content div div button { 269 | margin-top: 4rem !important; 270 | background-color: rgba(0, 0, 0, 0.73) !important; 271 | height: 1.5rem !important; 272 | width: 1.5rem !important; 273 | } 274 | 275 | .cart-content div div:first-of-type button { 276 | margin-left: 5rem !important; 277 | } 278 | 279 | .cart-content div div:last-of-type button { 280 | margin-right: -1rem !important; 281 | } 282 | 283 | @media (hover: hover) { 284 | .cart-go-back:hover { 285 | color: var(--c-primary); 286 | border-color: var(--c-primary); 287 | transition: all ease-in-out 0.3s; 288 | } 289 | .clear-cart-totals:hover { 290 | color: var(--c-primary); 291 | transition: all ease-in-out 0.3s; 292 | } 293 | } 294 | 295 | @media screen and (max-width: 600px) { 296 | .cart-content { 297 | width: 300px; 298 | } 299 | 300 | .cart-content div { 301 | width: 300px !important; 302 | height: 300px !important; 303 | } 304 | 305 | .cart-products-single { 306 | flex-direction: column; 307 | justify-content: initial; 308 | gap: 2rem; 309 | width: 100%; 310 | } 311 | } 312 | 313 | @media screen and (max-width: 500px) { 314 | .cart-totals ul { 315 | width: inherit; 316 | flex-direction: column; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/routes/checkout/Checkout.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import "./checkout.css" 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | 7 | const Checkout = ({ setOrderFormValue }) => { 8 | const [currentStep, setCurrentStep] = useState("details"); 9 | const [formValues, setFormValues] = useState({ firstname: "", lastname: "", email: "", phonenumber: "", streetaddress: "", country: "", postal: "", city: "", province: "", card: "", expiration: "", cvv: "" }) 10 | const [formErrors, setFormErrors] = useState({}); 11 | const navigate = useNavigate(); 12 | 13 | useEffect(() => { 14 | document.title = "Checkout | Shopping Time"; 15 | }, []); 16 | const setStep = (step, validationStep) => { 17 | setFormErrors(validateForm(formValues, validationStep)); 18 | if (Object.keys(validateForm(formValues, validationStep)).length > 0) { 19 | return; 20 | } 21 | else { 22 | setCurrentStep(step); 23 | } 24 | }; 25 | 26 | const validateForm = (values, step = "payment") => { 27 | const numberRegex = /\d/; 28 | const expiration = /^(0[1-9]|1[0-2])\/(2[2-9]|[3-9][0-9])$/; 29 | const cvvRegex3 = /^[0-9]{3}$/; 30 | const errors = {}; 31 | if (step === "details") { 32 | if (!values.firstname) { 33 | errors.firstname = "First name is required"; 34 | } 35 | else if (values.firstname.length < 3) { 36 | errors.firstname = "First name is invalid"; 37 | } 38 | else if (numberRegex.test(values.firstname)) { 39 | errors.firstname = "Last name is invalid"; 40 | } 41 | if (!values.lastname) { 42 | errors.lastname = "Last name is required"; 43 | } 44 | else if (values.lastname.length < 3) { 45 | errors.lastname = "Last name is invalid"; 46 | } 47 | else if (numberRegex.test(values.lastname)) { 48 | errors.lastname = "Last name is invalid"; 49 | } 50 | 51 | if (!values.email) { 52 | errors.email = "Please enter an email"; 53 | } 54 | else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) { 55 | errors.email = "Invalid email address"; 56 | } 57 | if (!values.phonenumber) { 58 | errors.phonenumber = "The phone number is required"; 59 | } 60 | else if (values.phonenumber.length < 5) { 61 | errors.phonenumber = "The phone number is invalid"; 62 | } 63 | else if (!numberRegex.test(values.phonenumber)) { 64 | errors.phonenumber = "The phone number is invalid"; 65 | } 66 | } 67 | if (step === "address") { 68 | if (!values.streetaddress) { 69 | errors.streetaddress = "Enter the street address"; 70 | } 71 | if (!values.country) { 72 | errors.country = "Please enter a country"; 73 | } 74 | else if (values.country.length < 3) { 75 | errors.country = "Please enter a valid country"; 76 | } 77 | else if (numberRegex.test(values.country)) { 78 | errors.country = "Please enter a valid country"; 79 | } 80 | if (!values.postal) { 81 | errors.postal = "Please enter a postal code"; 82 | } 83 | else if (values.postal.length < 3) { 84 | errors.postal = "Please enter a valid postal"; 85 | } 86 | if (!values.city) { 87 | errors.city = "Please enter the city"; 88 | } 89 | else if (values.city.length < 3) { 90 | errors.city = "Please enter a valid city"; 91 | } 92 | else if (numberRegex.test(values.city)) { 93 | errors.city = "Please enter a valid city"; 94 | } 95 | if (!values.province) { 96 | errors.province = "Please enter the province name"; 97 | } 98 | else if (values.province.length < 3) { 99 | errors.province = "Please enter a valid province"; 100 | } 101 | else if (numberRegex.test(values.province)) { 102 | errors.province = "Please enter a valid province"; 103 | } 104 | 105 | } 106 | if (step === "payment") { 107 | if (!values.card) { 108 | errors.card = "Card number is required"; 109 | } 110 | else if (values.card.length < 16) { 111 | errors.card = "Card number should be 16 characters"; 112 | } 113 | if (!values.expiration) { 114 | errors.expiration = "Enter the expiration date"; 115 | } 116 | else if (!expiration.test(values.expiration)) { 117 | errors.expiration = "Enter the valid expiration date"; 118 | } 119 | if (!values.cvv) { 120 | errors.cvv = "CVV is required"; 121 | } 122 | else if (!cvvRegex3.test(values.cvv)) { 123 | errors.cvv = "CVV is invalid"; 124 | } 125 | 126 | } 127 | return errors; 128 | } 129 | const submitForm = (e) => { 130 | e.preventDefault(); 131 | setFormErrors(validateForm(formValues)); 132 | if (Object.keys(validateForm(formValues)).length > 0) { 133 | return; 134 | } 135 | else { 136 | const today = new Date().toDateString(); 137 | setCurrentStep("done"); 138 | setOrderFormValue({ ...formValues, id: uuidv4(), date: today }); 139 | setFormValues({ firstname: "", lastname: "", email: "", phonenumber: "", streetaddress: "", country: "", postal: "", city: "", province: "", card: "", expiration: "", cvv: "" }) 140 | return navigate("/order"); 141 | } 142 | } 143 | 144 | const handleChange = (e) => { 145 | const { name, value } = e.target; 146 | setFormValues({ ...formValues, [name]: value }); 147 | } 148 | return ( 149 |
150 |

{currentStep === "done" ? "Thank you for ordering" : "Checkout"}

151 |
152 | {currentStep === "done" ? null : 153 |
    154 |
  • 1Personal details
  • 155 |
  • 2Delivery address
  • 156 |
  • 3Payment method
  • 157 |
} 158 | 159 |
160 | {currentStep === "details" ? 161 | 162 |

Personal details

163 |
164 | 171 | {formErrors.firstname} 172 | 179 | {formErrors.lastname} 180 | 186 | {formErrors.email} 187 | 194 | {formErrors.phonenumber} 195 |
196 |
: 197 | currentStep === "address" ? 198 | 199 |

Delivery information

200 |
201 | 208 | {formErrors.streetaddress} 209 | 216 | {formErrors.country} 217 | 223 | {formErrors.postal} 224 | 231 | {formErrors.city} 232 | 239 | {formErrors.province} 240 |
241 |
242 | : 243 | currentStep === "payment" ? 244 | 245 | 246 |

Payment

247 |
248 | 255 | {formErrors.card} 256 | 263 | {formErrors.expiration} 264 | 270 | {formErrors.cvv} 271 |
272 |
273 | : null} 274 | {currentStep === "details" ? 275 | 276 | : 277 | currentStep === "address" ? 278 |
279 | 280 | 281 |
: 282 | currentStep === "payment" ? 283 |
284 | 285 | 286 |
: 287 | null} 288 |
289 |
290 |
291 | 292 | ) 293 | } 294 | 295 | export default Checkout; -------------------------------------------------------------------------------- /src/routes/checkout/CheckoutSingleItem.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const CheckoutSingleItem = ({ singleProduct, selectedCurrency }) => { 4 | const [priceAmount, setPriceAmount] = useState(""); 5 | useEffect(() => { 6 | const targetCurrency = singleProduct.prices.filter((price) => price.currency.symbol === selectedCurrency)[0]; 7 | setPriceAmount(targetCurrency.amount.toFixed(2)); 8 | }, [selectedCurrency, singleProduct.prices]); 9 | 10 | return ( 11 |
  • {singleProduct.name} - {priceAmount}{selectedCurrency}, {singleProduct.quantity}
  • 12 | ) 13 | } -------------------------------------------------------------------------------- /src/routes/checkout/checkout.css: -------------------------------------------------------------------------------- 1 | .checkout { 2 | display: flex; 3 | flex-direction: column; 4 | margin-top: 3rem; 5 | padding: 2rem; 6 | margin-bottom: 4rem; 7 | } 8 | 9 | .checkout-content { 10 | display: flex; 11 | flex-direction: column; 12 | gap: 4rem; 13 | } 14 | 15 | .checkout h1 { 16 | font-family: var(--font-raleway); 17 | font-size: 3rem; 18 | letter-spacing: 1px; 19 | text-align: center; 20 | } 21 | 22 | .checkout-content .multi-steps { 23 | display: flex; 24 | flex-direction: row; 25 | list-style: none; 26 | gap: 4rem; 27 | justify-content: center; 28 | } 29 | 30 | .checkout-content .multi-steps li { 31 | display: flex; 32 | flex-direction: column; 33 | gap: 1rem; 34 | align-items: center; 35 | } 36 | 37 | .multi-steps .step-title { 38 | font-family: var(--font-raleway); 39 | color: grey; 40 | } 41 | 42 | .multi-steps .step-number { 43 | display: flex; 44 | flex-direction: column; 45 | text-align: center; 46 | justify-content: center; 47 | font-family: var(--font-raleway); 48 | color: black; 49 | font-weight: 600; 50 | border: 1px solid grey; 51 | border-radius: 50%; 52 | width: 1rem; 53 | height: 1rem; 54 | padding: 6px; 55 | } 56 | 57 | .multi-steps .step-number.active { 58 | background-color: var(--c-primary); 59 | border-color: var(--c-primary); 60 | } 61 | 62 | .checkout-content form { 63 | display: flex; 64 | flex-direction: column; 65 | gap: 2rem; 66 | width: 60%; 67 | } 68 | 69 | .checkout-content form h2 { 70 | font-family: var(--font-raleway); 71 | font-size: 2rem; 72 | letter-spacing: 1px; 73 | font-weight: 400; 74 | } 75 | 76 | .checkout-inputs { 77 | display: flex; 78 | flex-direction: column; 79 | gap: 1rem; 80 | } 81 | 82 | .checkout-inputs input { 83 | padding: 1rem; 84 | border: 1px solid black; 85 | } 86 | 87 | .checkout .form-buttons { 88 | display: flex; 89 | flex-direction: column; 90 | gap: 1rem; 91 | } 92 | 93 | .checkout .active-add-to-cart { 94 | flex-direction: column; 95 | align-items: center; 96 | padding: 1rem; 97 | color: var(--c-white); 98 | border: none; 99 | font-family: var(--font-raleway); 100 | font-weight: 600; 101 | font-size: 1rem; 102 | cursor: pointer; 103 | background-color: var(--c-primary); 104 | } 105 | 106 | .checkout .go-back { 107 | flex-direction: column; 108 | align-items: center; 109 | padding: 1rem 2rem; 110 | width: 100%; 111 | height: 3.25rem; 112 | color: black; 113 | border: none; 114 | font-family: var(--font-raleway); 115 | font-weight: 600; 116 | font-size: 1rem; 117 | cursor: pointer; 118 | opacity: 0.8; 119 | } 120 | 121 | .checkout .error { 122 | color: var(--c-primary); 123 | font-family: var(--font-raleway); 124 | font-weight: 100; 125 | } 126 | 127 | @media (max-width: 600px) { 128 | .checkout-content form { 129 | width: 90%; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/routes/landing/Landing.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import HeroCover from '../../assets/images/landing-images/hero-cover.webp' 4 | import "./landing.css" 5 | import { ResetLocation } from "../../helpers/ResetLocation"; 6 | 7 | const Landing = ({ changeCategory }) => { 8 | useEffect(() => { 9 | document.title = "Shopping Time"; 10 | }, []); 11 | return ( 12 |
    13 |
    14 |

    Building a better you!

    15 | 16 |
    17 |
    18 |
    19 | { 23 | changeCategory("blouses"); 24 | ResetLocation(); 25 | }} 26 | > 27 | Blouses 28 | 29 |
    30 |
    31 | { 34 | changeCategory("jeans"); 35 | ResetLocation(); 36 | }} 37 | className="custom-btn grid-button" 38 | > 39 | Jeans 40 | 41 |
    42 |
    43 | { 46 | changeCategory("shoes"); 47 | ResetLocation(); 48 | }} 49 | className=" custom-btn grid-button" 50 | > 51 | Shoes 52 | 53 |
    54 |
    55 | { 58 | changeCategory("dresses"); 59 | ResetLocation(); 60 | }} 61 | className="custom-btn grid-button" 62 | > 63 | Dresses 64 | 65 |
    66 |
    67 | { 70 | changeCategory("all"); 71 | ResetLocation(); 72 | }} 73 | className="custom-btn grid-button" 74 | > 75 | Store 76 | 77 |
    78 |
    79 |
    80 | ); 81 | } 82 | 83 | 84 | export default Landing; -------------------------------------------------------------------------------- /src/routes/landing/landing.css: -------------------------------------------------------------------------------- 1 | .landing .hero { 2 | height: 100vh; 3 | position: relative; 4 | overflow: hidden; 5 | } 6 | 7 | .landing .hero h2 { 8 | position: absolute; 9 | z-index: 3; 10 | font-family: var(--font-raleway); 11 | top: 23%; 12 | left: 47%; 13 | font-size: 4rem; 14 | letter-spacing: 1px; 15 | text-transform: uppercase; 16 | overflow: hidden; 17 | text-align: center; 18 | max-width: 42vw; 19 | } 20 | 21 | .landing .hero h2 span { 22 | color: var(--c-primary); 23 | } 24 | 25 | .landing .hero img { 26 | width: 100%; 27 | height: 100vh; 28 | object-fit: cover; 29 | } 30 | 31 | .landing .grid { 32 | display: grid; 33 | grid-template-columns: repeat(5, 1fr); 34 | grid-template-rows: repeat(3, 1fr); 35 | grid-column-gap: 5px; 36 | grid-row-gap: 5px; 37 | } 38 | 39 | .landing .grid .grid-one, 40 | .landing .grid .grid-two, 41 | .landing .grid .grid-three, 42 | .landing .grid .grid-four, 43 | .landing .grid .grid-five { 44 | width: 100%; 45 | position: relative; 46 | background-repeat: no-repeat; 47 | background-position: center; 48 | background-size: cover; 49 | height: 35rem; 50 | } 51 | 52 | .custom-btn { 53 | letter-spacing: 1px; 54 | width: 130px; 55 | height: 40px; 56 | color: black; 57 | padding: 10px 25px; 58 | font-family: "Lato", sans-serif; 59 | font-weight: 500; 60 | background: transparent; 61 | cursor: pointer; 62 | transition: all 0.3s ease; 63 | position: relative; 64 | display: inline-block; 65 | box-shadow: 66 | inset 2px 2px 2px 0px rgba(255, 255, 255, 0.5), 67 | 7px 7px 20px 0px rgba(0, 0, 0, 0.1), 68 | 4px 4px 5px 0px rgba(0, 0, 0, 0.1); 69 | outline: none; 70 | } 71 | 72 | .grid-one { 73 | grid-area: 1 / 1 / 2 / 4; 74 | background-image: url("../../assets/images/landing-images/gridOne.webp"); 75 | } 76 | 77 | .grid-two { 78 | grid-area: 1 / 4 / 2 / 6; 79 | background-image: url("../../assets/images/landing-images/gridTwo.webp"); 80 | } 81 | 82 | .grid-three { 83 | grid-area: 2 / 1 / 3 / 3; 84 | background-image: url("../../assets/images/landing-images/gridThree.webp"); 85 | } 86 | 87 | .grid-four { 88 | grid-area: 2 / 3 / 3 / 6; 89 | background-image: url("../../assets/images/landing-images/gridFour.webp"); 90 | } 91 | 92 | .grid-five { 93 | grid-area: 3 / 1 / 4 / 6; 94 | background-image: url("../../assets/images/landing-images/gridFive.webp"); 95 | } 96 | 97 | .grid-one .grid-button { 98 | top: 85%; 99 | right: -10%; 100 | } 101 | 102 | .grid-two .grid-button { 103 | top: 85%; 104 | right: -20%; 105 | } 106 | 107 | .grid-three .grid-button { 108 | top: 85%; 109 | left: 10%; 110 | } 111 | 112 | .grid-four .grid-button { 113 | top: 85%; 114 | right: -70%; 115 | } 116 | 117 | .grid-five .grid-button { 118 | bottom: -10%; 119 | right: -80%; 120 | } 121 | 122 | .grid-button { 123 | background-color: rgba(255, 255, 255, 0.287); 124 | backdrop-filter: blur(20px); 125 | background: linear-gradient(0deg, rgba(255, 255, 255, 0.287), rgb(209, 210, 210) 100%); 126 | width: 130px; 127 | height: 40px; 128 | line-height: 42px; 129 | padding: 0; 130 | border: none; 131 | text-decoration: none; 132 | text-align: center; 133 | } 134 | 135 | .grid-button span { 136 | position: relative; 137 | display: block; 138 | width: 100%; 139 | height: 100%; 140 | } 141 | 142 | .grid-button:before, 143 | .grid-button:after { 144 | position: absolute; 145 | content: ""; 146 | right: 0; 147 | top: 0; 148 | background: var(--c-text); 149 | transition: all 0.3s ease; 150 | } 151 | 152 | .grid-button:before { 153 | height: 0%; 154 | width: 2px; 155 | } 156 | 157 | .grid-button:after { 158 | width: 0%; 159 | height: 2px; 160 | } 161 | 162 | .grid-button span:before, 163 | .grid-button span:after { 164 | position: absolute; 165 | content: ""; 166 | left: 0; 167 | bottom: 0; 168 | background: var(--c-text); 169 | transition: all 0.3s ease; 170 | } 171 | 172 | .grid-button span:before { 173 | width: 2px; 174 | height: 0%; 175 | } 176 | 177 | .grid-button span:after { 178 | width: 0%; 179 | height: 2px; 180 | } 181 | 182 | @media (hover: hover) { 183 | .grid-one:hover, 184 | .landing .grid .grid-two:hover, 185 | .landing .grid .grid-three:hover, 186 | .landing .grid .grid-four:hover, 187 | .landing .grid .grid-five:hover { 188 | opacity: 0.8; 189 | } 190 | .grid-button:hover { 191 | background: transparent; 192 | box-shadow: none; 193 | } 194 | 195 | .grid-button:hover:before { 196 | height: 100%; 197 | } 198 | 199 | .grid-button:hover:after { 200 | width: 100%; 201 | } 202 | .grid-button span:hover { 203 | color: var(--c-text); 204 | } 205 | .grid-button span:hover:before { 206 | height: 100%; 207 | } 208 | 209 | .grid-button span:hover:after { 210 | width: 100%; 211 | } 212 | } 213 | 214 | @media (max-width: 750px) { 215 | .landing .hero h2 { 216 | top: 30%; 217 | left: 48%; 218 | font-size: 6vw; 219 | width: 100vw; 220 | text-align: right; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/routes/not-found/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import "./not-found.css"; 3 | const NotFound = () => { 4 | useEffect(() => { 5 | document.title = "404 | Shopping Time"; 6 | }, []); 7 | return ( 8 |
    9 | 14 |

    This page is not available

    15 |

    Sorry, we couldn’t find the page you’re looking for. 16 |

    17 |
    18 | ) 19 | } 20 | 21 | export default NotFound; -------------------------------------------------------------------------------- /src/routes/not-found/not-found.css: -------------------------------------------------------------------------------- 1 | .not-found { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | font-family: var(--font-raleway); 6 | color: black; 7 | padding: 2rem; 8 | text-align: center; 9 | } 10 | .not-found img { 11 | width: 20rem; 12 | height: 20rem; 13 | } 14 | -------------------------------------------------------------------------------- /src/routes/order/Order.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { CheckoutSingleItem } from "../checkout/CheckoutSingleItem"; 3 | import "./order.css"; 4 | import { useNavigate } from "react-router-dom"; 5 | import { ResetLocation } from "../../helpers/ResetLocation"; 6 | 7 | 8 | const Order = ({ orderFormValue, cartItems, selectedCurrency, clearCart }) => { 9 | const [showConfirmation, setShowConfirmation] = useState(false); 10 | const navigate = useNavigate(); 11 | useEffect(() => { 12 | document.title = "Order summary | Shopping Time"; 13 | }, []); 14 | 15 | const handlePrint = () => { 16 | window.print(); 17 | }; 18 | 19 | const handleContinueShopping = () => { 20 | setShowConfirmation(false); 21 | clearCart(); 22 | navigate("/"); 23 | ResetLocation(); 24 | }; 25 | 26 | const handleLeavePage = () => { 27 | setShowConfirmation(true); 28 | }; 29 | 30 | const handleCancelLeavePage = () => { 31 | setShowConfirmation(false); 32 | }; 33 | 34 | 35 | return ( 36 |
    37 |

    Order summary

    38 |
    39 |

    Dear {orderFormValue.firstname} {orderFormValue.lastname},

    40 |

    41 | Your order has been received successfully at Shopping Time. We're thrilled that you've chosen us for your fashion needs. Your style journey is about to begin! 42 |

    43 |

    Order Details:

    44 |
      45 |
    • Order Number: {orderFormValue.id}
    • 46 |
    • Order Date: {orderFormValue.date}
    • 47 |
    • Shipping Address: {orderFormValue.country}, {orderFormValue.city}, {orderFormValue.province}, {orderFormValue.postal}, {orderFormValue.streetaddress}
    • 48 |
    • Payment Method: Bank card
    • 49 |
    50 |

    Items Ordered:

    51 |
      52 | {cartItems.map((singleProduct) => { 53 | return ( 54 | 59 | ); 60 | })} 61 |
    62 |

    Delivery Information:

    63 |

    64 | 65 | Your items will be carefully prepared and dispatched for delivery. You will receive a confirmation email with tracking information once your order ships. 66 |

    67 |

    Stay Connected:

    68 | If you have any questions or need assistance, feel free to contact our customer support team at support@shoppingtime.com or +1 1234 344 2342. 69 |
    70 |

    Happy Shopping! 71 |

    72 |

    Warm regards,
    73 | The Shopping Time Team

    74 |
    75 |
    76 | 77 | 78 |
    79 | {showConfirmation && ( 80 |
    81 |
    82 |

    Are you sure you want to leave this page?

    83 |
    84 | 85 | 86 |
    87 |
    88 |
    89 | )} 90 |
    91 | ) 92 | } 93 | 94 | 95 | export default Order; -------------------------------------------------------------------------------- /src/routes/order/order.css: -------------------------------------------------------------------------------- 1 | .order, 2 | .order-success { 3 | display: flex; 4 | flex-direction: column; 5 | font-family: var(--font-raleway); 6 | } 7 | 8 | .order { 9 | gap: 2rem; 10 | margin-top: 3rem; 11 | padding: 2rem; 12 | margin-bottom: 4rem; 13 | position: relative; 14 | } 15 | 16 | .order h2, 17 | .order-success h2 { 18 | font-size: 3rem; 19 | letter-spacing: 1px; 20 | text-align: center; 21 | } 22 | 23 | .order-success { 24 | gap: 1rem; 25 | width: 100%; 26 | margin: 0 auto; 27 | } 28 | 29 | .order-success h2 { 30 | font-size: 2rem; 31 | } 32 | 33 | .order-success h3 { 34 | font-size: 1rem; 35 | font-weight: 600; 36 | } 37 | 38 | .print, 39 | .exit-order { 40 | flex-direction: column; 41 | align-items: center; 42 | padding: 1rem 2rem; 43 | width: max-content; 44 | height: 3.25rem; 45 | font-family: var(--font-raleway); 46 | font-weight: 600; 47 | font-size: 1rem; 48 | cursor: pointer; 49 | } 50 | 51 | .print { 52 | color: var(--c-white); 53 | border: none; 54 | background-color: var(--c-primary); 55 | } 56 | 57 | .exit-order { 58 | color: gray; 59 | border: none; 60 | opacity: 0.8; 61 | } 62 | 63 | .order-details, 64 | .items-ordered { 65 | list-style: none; 66 | } 67 | 68 | .confirmation-dialog-inner .buttons button { 69 | display: flex; 70 | flex-direction: column; 71 | align-items: center; 72 | padding: 10px; 73 | width: max-content; 74 | color: var(--c-white); 75 | border: none; 76 | font-family: var(--font-raleway); 77 | font-weight: 600; 78 | cursor: pointer; 79 | background-color: var(--c-primary); 80 | } 81 | 82 | .confirmation-dialog { 83 | position: fixed; 84 | background-color: rgba(0, 0, 0, 0.693); 85 | backdrop-filter: blur(2px); 86 | height: 100vh; 87 | bottom: 0; 88 | top: 0; 89 | left: 0; 90 | width: 100%; 91 | display: flex; 92 | flex-direction: column; 93 | justify-content: center; 94 | align-items: center; 95 | } 96 | 97 | .confirmation-dialog-inner { 98 | display: flex; 99 | flex-direction: column; 100 | gap: 2rem; 101 | background-color: white; 102 | padding: 2rem; 103 | } 104 | 105 | .confirmation-dialog-inner h3 { 106 | font-weight: 400; 107 | } 108 | -------------------------------------------------------------------------------- /src/routes/single-product/ProductShowcase.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ProductShowcase = ({ singleProduct }) => { 4 | return ( 5 | 6 | {singleProduct && singleProduct.gallery && ( 7 |
    8 | {singleProduct.gallery.map((image, index) => ( 9 | 10 | ))} 11 |
    12 | )} 13 |
    14 | ); 15 | } 16 | 17 | export default ProductShowcase; -------------------------------------------------------------------------------- /src/routes/single-product/ProductTitles.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ProductTitles = ({ singleProduct }) => { 4 | return ( 5 |
    6 |

    7 | Store / {singleProduct.category} 8 |

    9 |

    {singleProduct.brand}

    10 |

    {singleProduct.name}

    11 |
    12 | ); 13 | } 14 | 15 | export default ProductTitles; 16 | -------------------------------------------------------------------------------- /src/routes/single-product/SingleProduct.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from "react"; 2 | import Attribute from "../../components/attributes/Attributes.jsx"; 3 | import AddToCartButton from "../../components/AddToCartButton.jsx"; 4 | import ProductShowcase from "./ProductShowcase.jsx"; 5 | import ProductTitles from "./ProductTitles.jsx"; 6 | import { ResetLocation } from "../../helpers/ResetLocation.jsx"; 7 | 8 | const SingleProduct = ({ selectedCurrency, handleAddProduct, alertMessageMain, allProducts }) => { 9 | const [selectedAttributes, setSelectedAttributes] = useState([]); 10 | const [allAttributesAreSelected, setAllAttributesAreSelected] = useState(false); 11 | const [singleProduct, setSingleProduct] = useState({}); 12 | const [priceAmount, setPriceAmount] = useState(""); 13 | const [isLoading, setIsLoading] = useState(false) 14 | 15 | useEffect(() => { 16 | document.title = `${singleProduct.name} | Shopping Time`; 17 | ResetLocation(); 18 | }, [singleProduct]); 19 | 20 | const filterCurrency = useCallback((product, currency) => { 21 | const price = product?.prices?.find((price) => price.currency.symbol === currency); 22 | setPriceAmount(price?.amount?.toFixed(2)); 23 | }, []); 24 | 25 | const getProductById = useCallback((uniqueId) => { 26 | 27 | const targetProduct = allProducts.find((item) => item.id === uniqueId); 28 | setIsLoading(true) 29 | if (targetProduct) { 30 | setSingleProduct(targetProduct); 31 | filterCurrency(targetProduct, selectedCurrency); 32 | 33 | if (targetProduct.attributes.length === 0) { 34 | setAllAttributesAreSelected(true); 35 | } 36 | setIsLoading(false) 37 | if (singleProduct.description) { 38 | document.querySelector(".description").innerHTML = singleProduct.description; 39 | } 40 | } 41 | 42 | }, [selectedCurrency, filterCurrency, allProducts, singleProduct.description]); 43 | 44 | useEffect(() => { 45 | const pathname = window.location.pathname.toString().substring(7); 46 | getProductById(pathname); 47 | 48 | }, [getProductById]); 49 | 50 | 51 | 52 | 53 | const handleSelectedAttributes = (attributeId, attributeValue) => { 54 | const newSelectedAttribute = { attributeId, attributeValue }; 55 | const userSelectedAttributes = [...selectedAttributes]; 56 | const existingAttributeIndex = userSelectedAttributes.findIndex( 57 | (attribute) => attribute.attributeId === newSelectedAttribute.attributeId 58 | ); 59 | 60 | if (existingAttributeIndex !== -1) { 61 | userSelectedAttributes[existingAttributeIndex] = newSelectedAttribute; 62 | } else { 63 | userSelectedAttributes.push(newSelectedAttribute); 64 | } 65 | setSelectedAttributes(userSelectedAttributes); 66 | 67 | if (userSelectedAttributes.length === singleProduct.attributes.length) { 68 | setAllAttributesAreSelected(true); 69 | } 70 | 71 | }; 72 | return ( 73 |
    74 | {isLoading ? 75 |
    76 |

    The product is loading, please wait...

    77 |
    78 | : 79 | 80 |
    81 | 82 |
    83 | {singleProduct?.attributes?.map((attribute) => ( 84 | 91 | ))} 92 |
    93 |

    Price:

    94 |
    95 |

    96 | {selectedCurrency} 97 | {priceAmount} 98 |

    99 |
    100 |
    101 | 110 |
    111 |
    112 |
    113 |
    } 114 |
    115 | ); 116 | } 117 | 118 | 119 | 120 | 121 | export default SingleProduct; -------------------------------------------------------------------------------- /src/routes/single-product/single-product.css: -------------------------------------------------------------------------------- 1 | .single-product { 2 | display: flex; 3 | flex-direction: column; 4 | margin: 0 auto; 5 | width: 90%; 6 | margin-bottom: 3.5rem; 7 | justify-content: center; 8 | gap: 3rem; 9 | } 10 | 11 | .single-product-titles { 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | margin: 0 auto; 16 | padding: 8rem; 17 | } 18 | .single-products__loader { 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | height: 100vh; 24 | } 25 | .single-products__loader h3 { 26 | font-size: 1.5rem; 27 | font-weight: 400; 28 | } 29 | .addtocart .active-add-to-cart, 30 | .addtocart .inactive-add-to-cart { 31 | width: max-content; 32 | } 33 | 34 | .single-product-titles .category { 35 | text-transform: uppercase; 36 | font-family: var(--font-roboto); 37 | } 38 | 39 | .product-showcase { 40 | display: grid; 41 | grid-template-columns: repeat(4, 1fr); 42 | align-items: start; 43 | } 44 | 45 | .product-showcase img { 46 | max-width: 100%; 47 | height: 20rem; 48 | object-fit: cover; 49 | } 50 | 51 | .single-product .data { 52 | flex-direction: column; 53 | gap: 2rem; 54 | } 55 | 56 | .single-product-titles .brand { 57 | font-weight: 600; 58 | font-size: 1.875rem; 59 | } 60 | 61 | .single-product-titles .name { 62 | font-weight: 400; 63 | font-size: 1.875rem; 64 | line-height: 3rem; 65 | } 66 | 67 | .single-product-price p { 68 | font-weight: 700; 69 | } 70 | 71 | .single-product-price { 72 | flex-direction: column; 73 | gap: 1rem; 74 | } 75 | 76 | .single-product .attribute { 77 | flex-direction: column; 78 | gap: 2rem; 79 | } 80 | 81 | .pricing-section { 82 | flex-direction: column; 83 | margin-top: 0.9rem; 84 | gap: 0.8rem; 85 | } 86 | 87 | .pricing-section h3 { 88 | font-weight: 700; 89 | font-size: 1.5rem; 90 | color: var(--c-text); 91 | font-family: var(--font-roboto); 92 | } 93 | 94 | .single-product .description { 95 | flex-direction: column; 96 | font-family: var(--font-roboto); 97 | font-weight: 400; 98 | font-size: 1rem; 99 | color: var(--c-text); 100 | line-height: 1.5rem; 101 | } 102 | 103 | .single-product-pricing .product-price { 104 | font-weight: 700; 105 | font-size: 1.5rem; 106 | } 107 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/index.html" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | base: '/', 6 | plugins: [react()], 7 | server: { 8 | historyApiFallback: true 9 | } 10 | }); 11 | --------------------------------------------------------------------------------