├── .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 | 
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 | {
10 | handleAddProduct(item, selectedAttributes);
11 | alertMessageMain();
12 | if (className === 'quick-addtocart') {
13 | setActiveItem(null)
14 | }
15 | }}
16 | className={
17 | item.inStock && allAttributesAreSelected
18 | ? "active-add-to-cart"
19 | : "inactive-add-to-cart"
20 | }
21 | disabled={!item.inStock || !allAttributesAreSelected}
22 | >
23 | {item.inStock ? "ADD TO CART" : "OUT OF STOCK"}
24 |
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 |
8 | handleAddProduct(
9 | singleProduct,
10 | singleProduct.userSelectedAttributes
11 | )
12 | }
13 | >
14 | +
15 |
16 | {singleProduct.quantity}
17 |
19 | handleRemoveProduct(
20 | singleProduct,
21 | singleProduct.userSelectedAttributes
22 | )
23 | }
24 | >
25 | -
26 |
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 | setSelectedAttribute(item.value)}
23 | style={{
24 | backgroundColor: `${item.value}`,
25 | border: `1px solid ${item.value}`
26 | }}>C
27 | ) : (
28 | setSelectedAttribute(item.value)}
31 | style={selectedAttribute === item.value || item.isSelected === true
32 | ? {
33 | background: "black",
34 | color: "white"
35 | }
36 | : null
37 | }>{item.value}
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 |
75 | {item.value}
76 |
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 |
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 | 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 | { clearCart(); removeCartOverlay() }} className="clear-cart" to="/checkout">
54 | Clear cart
55 |
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 |
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 |
17 |
18 | {
21 | changeCategory("");
22 | closeMenu();
23 | }}
24 | className="home-link">
25 | Home
26 |
27 |
28 | {allCategories.map((category) => (
29 | {
32 | changeCategory(category.name);
33 | closeMenu();
34 | }}>
35 |
40 | {category.name}
41 |
42 |
43 | ))}
44 |
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 |
54 |
55 |
56 | {
59 | changeCategory("");
60 | closeMenu();
61 | }}
62 | className="header-one">
63 | Shopping Time
64 |
65 |
71 |
92 |
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 { clearCart() }} className="clear-cart-totals" to="/checkout">
8 | Clear cart
9 |
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 | 1 Personal details
155 | 2 Delivery address
156 | 3 Payment method
157 | }
158 |
159 |
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 | Print
77 | Continue shopping
78 |
79 | {showConfirmation && (
80 |
81 |
82 | Are you sure you want to leave this page?
83 |
84 | Continue shopping
85 | Cancel
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 |
--------------------------------------------------------------------------------